dcs_liberation/game/layout/layoutloader.py
RndName 2c17a9a52e Refactor Templates to Layouts, Review and Cleanup
- Fix tgogenerator
- Fix UI for ForceGroup and Layouts
- Fix ammo depot handling
- Split bigger files in smaller meaningful files (TGO, layouts, forces)
- Renamed Template to Layout
- Renamed GroundGroup to TheaterGroup and GroundUnit to TheaterUnit
- Reorganize Layouts and UnitGroups to a ArmedForces class and ForceGroup similar to the AirWing and Squadron
- Reworded the UnitClass, GroupRole, GroupTask (adopted to PEP8) and reworked the connection from Role and Task
- added comments
- added missing unit classes
- added temp workaround for missing classes
- add repariable property to TheaterUnit
- Review and Cleanup

Added serialization for loaded templates

Loading the templates from the .miz files takes a lot of computation time and in the future there will be more templates added to the system. Therefore a local pickle serialization for the loaded templates was re-added:
- The pickle will be created the first time the TemplateLoader will be accessed
- Pickle is stored in Liberation SaveDir
- Added UI option to (re-)import templates
2022-02-21 20:45:41 +01:00

204 lines
7.9 KiB
Python

from __future__ import annotations
import itertools
import logging
import pickle
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Iterator
import dcs
import yaml
from dcs import Point
from dcs.unitgroup import StaticGroup
from game import persistency
from game.data.groups import GroupRole, GroupTask
from game.layout.layout import (
TheaterLayout,
GroupLayout,
LayoutUnit,
AntiAirLayout,
BuildingLayout,
NavalLayout,
GroundForceLayout,
DefensesLayout,
)
from game.layout.layoutmapping import GroupLayoutMapping, LayoutMapping
from game.profiling import logged_duration
from game.version import VERSION
TEMPLATE_DIR = "resources/layouts/"
TEMPLATE_DUMP = "Liberation/layouts.p"
TEMPLATE_TYPES = {
GroupRole.AIR_DEFENSE: AntiAirLayout,
GroupRole.BUILDING: BuildingLayout,
GroupRole.NAVAL: NavalLayout,
GroupRole.GROUND_FORCE: GroundForceLayout,
GroupRole.DEFENSES: DefensesLayout,
}
class LayoutLoader:
# list of layouts per category. e.g. AA or similar
_templates: dict[str, TheaterLayout] = {}
def __init__(self) -> None:
self._templates = {}
def initialize(self) -> None:
if not self._templates:
with logged_duration("Loading layouts"):
self.load_templates()
@property
def layouts(self) -> Iterator[TheaterLayout]:
self.initialize()
yield from self._templates.values()
def load_templates(self) -> None:
"""This will load all pre-loaded layouts from a pickle file.
If pickle can not be loaded it will import and dump the layouts"""
# We use a pickle for performance reasons. Importing takes many seconds
file = Path(persistency.base_path()) / TEMPLATE_DUMP
if file.is_file():
# Load from pickle if existing
with file.open("rb") as f:
try:
version, self._templates = pickle.load(f)
# Check if the game version of the dump is identical to the current
if version == VERSION:
return
except Exception as e:
logging.error(f"Error {e} reading layouts dump. Recreating.")
# If no dump is available or game version is different create a new dump
self.import_templates()
def import_templates(self) -> None:
"""This will import all layouts from the template folder
and dumps them to a pickle"""
mappings: dict[str, list[LayoutMapping]] = {}
with logged_duration("Parsing mapping yamls"):
for file in Path(TEMPLATE_DIR).rglob("*.yaml"):
if not file.is_file():
continue
with file.open("r", encoding="utf-8") as f:
mapping_dict = yaml.safe_load(f)
template_map = LayoutMapping.from_dict(mapping_dict, f.name)
if template_map.layout_file in mappings:
mappings[template_map.layout_file].append(template_map)
else:
mappings[template_map.layout_file] = [template_map]
with logged_duration(f"Parsing all layout miz multithreaded"):
with ThreadPoolExecutor() as exe:
for miz, maps in mappings.items():
exe.submit(self._load_from_miz, miz, maps)
logging.info(f"Imported {len(self._templates)} layouts")
self._dump_templates()
def _dump_templates(self) -> None:
file = Path(persistency.base_path()) / TEMPLATE_DUMP
dump = (VERSION, self._templates)
with file.open("wb") as fdata:
pickle.dump(dump, fdata)
@staticmethod
def mapping_for_group(
mappings: list[LayoutMapping], group_name: str
) -> tuple[LayoutMapping, int, GroupLayoutMapping]:
for mapping in mappings:
for g_id, group_mapping in enumerate(mapping.groups):
if (
group_mapping.name == group_name
or group_name in group_mapping.statics
):
return mapping, g_id, group_mapping
raise KeyError
def _load_from_miz(self, miz: str, mappings: list[LayoutMapping]) -> None:
template_position: dict[str, Point] = {}
temp_mis = dcs.Mission()
with logged_duration(f"Parsing {miz}"):
# The load_file takes a lot of time to compute. That's why the layouts
# are written to a pickle and can be reloaded from the ui
# Example the whole routine: 0:00:00.934417,
# the .load_file() method: 0:00:00.920409
temp_mis.load_file(miz)
for country in itertools.chain(
temp_mis.coalition["red"].countries.values(),
temp_mis.coalition["blue"].countries.values(),
):
for dcs_group in itertools.chain(
temp_mis.country(country.name).vehicle_group,
temp_mis.country(country.name).ship_group,
temp_mis.country(country.name).static_group,
):
try:
mapping, group_id, group_mapping = self.mapping_for_group(
mappings, dcs_group.name
)
except KeyError:
logging.warning(f"No mapping for dcs group {dcs_group.name}")
continue
template = self._templates.get(mapping.name, None)
if template is None:
# Create a new template
template = TEMPLATE_TYPES[mapping.role](
mapping.name, mapping.role, mapping.description
)
template.generic = mapping.generic
template.tasks = mapping.tasks
self._templates[template.name] = template
for i, unit in enumerate(dcs_group.units):
group_template = None
for group in template.groups:
if group.name == group_mapping.name:
# We already have a layoutgroup for this dcs_group
group_template = group
if not group_template:
group_template = GroupLayout(
group_mapping.name,
[],
group_mapping.group,
group_mapping.unit_count,
group_mapping.unit_types,
group_mapping.unit_classes,
group_mapping.alternative_classes,
)
group_template.optional = group_mapping.optional
# Add the group at the correct position
template.add_group(group_template, group_id)
unit_template = LayoutUnit.from_unit(unit)
if i == 0 and template.name not in template_position:
template_position[template.name] = unit.position
unit_template.position = (
unit_template.position - template_position[template.name]
)
group_template.units.append(unit_template)
def by_name(self, template_name: str) -> Iterator[TheaterLayout]:
for template in self.layouts:
if template.name == template_name:
yield template
def by_task(self, group_task: GroupTask) -> Iterator[TheaterLayout]:
for template in self.layouts:
if not group_task or group_task in template.tasks:
yield template
def by_tasks(self, group_tasks: list[GroupTask]) -> Iterator[TheaterLayout]:
unique_templates = []
for group_task in group_tasks:
for template in self.by_task(group_task):
if template not in unique_templates:
unique_templates.append(template)
yield from unique_templates