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
This commit is contained in:
RndName
2022-02-10 12:23:16 +01:00
parent 1ae6503ceb
commit 2c17a9a52e
138 changed files with 1985 additions and 3096 deletions

View File

@@ -1,21 +1,22 @@
from __future__ import annotations
import copy
import itertools
import logging
import random
from dataclasses import dataclass, field
from functools import cached_property
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
import dcs
from dcs.countries import country_dict
from dcs.unittype import ShipType
from dcs.unittype import ShipType, StaticType
from dcs.unittype import UnitType as DcsUnitType
from game.data.building_data import (
WW2_ALLIES_BUILDINGS,
DEFAULT_AVAILABLE_BUILDINGS,
WW2_GERMANY_BUILDINGS,
WW2_FREE,
REQUIRED_BUILDINGS,
)
from game.data.doctrine import (
Doctrine,
@@ -24,18 +25,12 @@ from game.data.doctrine import (
WWII_DOCTRINE,
)
from game.data.units import UnitClass
from game.data.groups import GroupRole, GroupTask
from game import db
from game.data.groups import GroupRole
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unitgroup import UnitGroup
from game.armedforces.forcegroup import ForceGroup
from game.dcs.unittype import UnitType
from gen.templates import (
GroundObjectTemplates,
GroundObjectTemplate,
GroupTemplate,
)
if TYPE_CHECKING:
from game.theater.start_generator import ModSettings
@@ -84,7 +79,7 @@ class Faction:
air_defense_units: List[GroundUnitType] = field(default_factory=list)
# A list of all supported sets of units
preset_groups: list[UnitGroup] = field(default_factory=list)
preset_groups: list[ForceGroup] = field(default_factory=list)
# Possible Missile site generators for this faction
missiles: List[GroundUnitType] = field(default_factory=list)
@@ -110,7 +105,7 @@ class Faction:
# doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
# List of available building templates for this faction
# List of available building layouts for this faction
building_set: List[str] = field(default_factory=list)
# List of default livery overrides
@@ -125,47 +120,24 @@ class Faction:
#: both will use it.
unrestricted_satnav: bool = False
# All possible templates which can be generated by the faction
templates: GroundObjectTemplates = field(default=GroundObjectTemplates())
# All available unit_groups
unit_groups: dict[GroupRole, list[UnitGroup]] = field(default_factory=dict)
# Save all accessible units for performance increase
_accessible_units: list[UnitType[Any]] = field(default_factory=list)
def __getitem__(self, item: str) -> Any:
return getattr(self, item)
@property
def accessible_units(self) -> Iterator[UnitType[Any]]:
yield from self._accessible_units
@property
def air_defenses(self) -> list[str]:
"""Returns the Air Defense types"""
air_defenses = [a.name for a in self.air_defense_units]
air_defenses.extend(
[pg.name for pg in self.preset_groups if pg.role == GroupRole.AntiAir]
)
return sorted(air_defenses)
def has_access_to_unit_type(self, unit_type: str) -> bool:
# GroundUnits
if any(unit_type == u.dcs_id for u in self.accessible_units):
def has_access_to_dcs_type(self, unit_type: Type[DcsUnitType]) -> bool:
# Vehicle and Ship Units
if any(unit_type == u.dcs_unit_type for u in self.accessible_units):
return True
# Statics
if db.static_type_from_name(unit_type) is not None:
if issubclass(unit_type, StaticType):
# TODO Improve the statics checking
# We currently do not have any list or similar to check if a faction has
# access to a specific static. There we accept any static here
return True
return False
def has_access_to_unit_class(self, unit_class: UnitClass) -> bool:
return any(unit.unit_class is unit_class for unit in self.accessible_units)
def _load_accessible_units(self, templates: GroundObjectTemplates) -> None:
self._accessible_units = []
@cached_property
def accessible_units(self) -> list[UnitType[Any]]:
all_units: Iterator[UnitType[Any]] = itertools.chain(
self.ground_units,
self.infantry_units,
@@ -173,138 +145,22 @@ class Faction:
self.naval_units,
self.missiles,
(
ground_unit
unit
for preset_group in self.preset_groups
for ground_unit in preset_group.ground_units
),
(
ship_unit
for preset_group in self.preset_groups
for ship_unit in preset_group.ship_units
for unit in preset_group.units
),
)
for unit in all_units:
if unit not in self._accessible_units:
self._accessible_units.append(unit)
return list(all_units)
def initialize(
self, all_templates: GroundObjectTemplates, mod_settings: ModSettings
) -> None:
# Apply the mod settings
self._apply_mod_settings(mod_settings)
# Load all accessible units and store them for performant later usage
self._load_accessible_units(all_templates)
# Load all faction compatible templates
self._load_templates(all_templates)
# Load Unit Groups
self._load_unit_groups()
def _add_unit_group(self, unit_group: UnitGroup, merge: bool = True) -> None:
if not unit_group.templates:
unit_group.load_templates(self)
if not unit_group.templates:
# Empty templates will throw an error on generation
logging.error(
f"Skipping Unit group {unit_group.name} as no templates are available to generate the group"
)
return
if unit_group.role in self.unit_groups:
for group in self.unit_groups[unit_group.role]:
if merge and all(task in group.tasks for task in unit_group.tasks):
# Update existing group if same tasking
group.update_from_unit_group(unit_group)
return
# Add new Unit_group
self.unit_groups[unit_group.role].append(unit_group)
else:
self.unit_groups[unit_group.role] = [unit_group]
def _load_unit_groups(self) -> None:
# This function will create all the UnitGroups for the faction
# It will create a unit group for each global Template, Building or
# Legacy supported templates (not yet migrated from the generators).
# For every preset_group there will be a separate UnitGroup so no mixed
# UnitGroups will be generated for them. Special groups like complex SAM Systems
self.unit_groups = {}
# Generate UnitGroups for all global templates
for role, template in self.templates.templates:
if template.generic or role == GroupRole.Building:
# Build groups for global templates and buildings
self._add_group_for_template(role, template)
# Add preset groups
for preset_group in self.preset_groups:
# Add as separate group, do not merge with generic groups!
self._add_unit_group(preset_group, False)
def _add_group_for_template(
self, role: GroupRole, template: GroundObjectTemplate
) -> None:
unit_group = UnitGroup(
f"{role.value}: {', '.join([t.value for t in template.tasks])}",
[u for u in template.units if isinstance(u, GroundUnitType)],
[u for u in template.units if isinstance(u, ShipUnitType)],
list(template.statics),
role,
@property
def air_defenses(self) -> list[str]:
"""Returns the Air Defense types"""
# This is used for the faction overview in NewGameWizard
air_defenses = [a.name for a in self.air_defense_units]
air_defenses.extend(
[pg.name for pg in self.preset_groups if pg.role == GroupRole.AIR_DEFENSE]
)
unit_group.tasks = template.tasks
unit_group.set_templates([template])
self._add_unit_group(unit_group)
def initialize_group_template(
self, group: GroupTemplate, faction_sensitive: bool = True
) -> bool:
# Sensitive defines if the initialization should check if the unit is available
# to this faction or not. It is disabled for migration only atm.
unit_types = [
t
for t in group.unit_types
if not faction_sensitive or self.has_access_to_unit_type(t)
]
alternative_types = []
for accessible_unit in self.accessible_units:
if accessible_unit.unit_class in group.unit_classes:
unit_types.append(accessible_unit.dcs_id)
if accessible_unit.unit_class in group.alternative_classes:
alternative_types.append(accessible_unit.dcs_id)
if not unit_types and not alternative_types and not group.optional:
raise StopIteration
types = unit_types or alternative_types
group.set_possible_types(types)
return len(types) > 0
def _load_templates(self, all_templates: GroundObjectTemplates) -> None:
self.templates = GroundObjectTemplates()
# This loads all templates which are usable by the faction
for role, template in all_templates.templates:
# Make a deep copy of a template and add it to the template_list.
# This is required to have faction independent templates. Otherwise
# the reference would be the same and changes would affect all.
faction_template = copy.deepcopy(template)
try:
faction_template.groups[:] = [
group_template
for group_template in faction_template.groups
if self.initialize_group_template(group_template)
]
if (
role == GroupRole.Building
and GroupTask.StrikeTarget in template.tasks
and faction_template.category not in self.building_set
):
# Special handling for strike targets. Skip if not supported by faction
continue
if faction_template.groups:
self.templates.add_template(role, faction_template)
continue
except StopIteration:
pass
logging.info(f"{self.name} can not use template {template.name}")
return sorted(air_defenses)
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@@ -350,8 +206,13 @@ class Faction:
]
faction.missiles = [GroundUnitType.named(n) for n in json.get("missiles", [])]
faction.naval_units = [
ShipUnitType.named(n) for n in json.get("naval_units", [])
]
# This has to be loaded AFTER GroundUnitType and ShipUnitType to work properly
faction.preset_groups = [
UnitGroup.named(n) for n in json.get("preset_groups", [])
ForceGroup.named(n) for n in json.get("preset_groups", [])
]
faction.requirements = json.get("requirements", {})
@@ -359,10 +220,6 @@ class Faction:
faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
faction.naval_units = [
ShipUnitType.named(n) for n in json.get("naval_units", [])
]
faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None)
if jtac_name is not None:
@@ -394,6 +251,9 @@ class Faction:
else:
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
# Add required buildings for the game logic (e.g. ammo, factory..)
faction.building_set.extend(REQUIRED_BUILDINGS)
# Load liveries override
faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {})
@@ -403,9 +263,6 @@ class Faction:
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
# Templates
faction.templates = GroundObjectTemplates()
return faction
@property
@@ -419,44 +276,7 @@ class Faction:
if unit.unit_class is unit_class:
yield unit
def groups_for_role_and_task(
self, group_role: GroupRole, group_task: Optional[GroupTask] = None
) -> list[UnitGroup]:
if group_role not in self.unit_groups:
return []
groups = []
for unit_group in self.unit_groups[group_role]:
if not group_task or group_task in unit_group.tasks:
groups.append(unit_group)
return groups
def groups_for_role_and_tasks(
self, group_role: GroupRole, tasks: list[GroupTask]
) -> list[UnitGroup]:
groups = []
for task in tasks:
for group in self.groups_for_role_and_task(group_role, task):
if group not in groups:
groups.append(group)
return groups
def random_group_for_role(self, group_role: GroupRole) -> Optional[UnitGroup]:
unit_groups = self.groups_for_role_and_task(group_role)
return random.choice(unit_groups) if unit_groups else None
def random_group_for_role_and_task(
self, group_role: GroupRole, group_task: GroupTask
) -> Optional[UnitGroup]:
unit_groups = self.groups_for_role_and_task(group_role, group_task)
return random.choice(unit_groups) if unit_groups else None
def random_group_for_role_and_tasks(
self, group_role: GroupRole, tasks: list[GroupTask]
) -> Optional[UnitGroup]:
unit_groups = self.groups_for_role_and_tasks(group_role, tasks)
return random.choice(unit_groups) if unit_groups else None
def _apply_mod_settings(self, mod_settings: ModSettings) -> None:
def apply_mod_settings(self, mod_settings: ModSettings) -> None:
# aircraft
if not mod_settings.a4_skyhawk:
self.remove_aircraft("A-4E-C")
@@ -516,20 +336,20 @@ class Faction:
self.remove_vehicle("KORNET")
# high digit sams
if not mod_settings.high_digit_sams:
self.remove_presets("SA-10B/S-300PS")
self.remove_presets("SA-12/S-300V")
self.remove_presets("SA-20/S-300PMU-1")
self.remove_presets("SA-20B/S-300PMU-2")
self.remove_presets("SA-23/S-300VM")
self.remove_presets("SA-17")
self.remove_presets("KS-19")
self.remove_preset("SA-10B/S-300PS")
self.remove_preset("SA-12/S-300V")
self.remove_preset("SA-20/S-300PMU-1")
self.remove_preset("SA-20B/S-300PMU-2")
self.remove_preset("SA-23/S-300VM")
self.remove_preset("SA-17")
self.remove_preset("KS-19")
def remove_aircraft(self, name: str) -> None:
for i in self.aircrafts:
if i.dcs_unit_type.id == name:
self.aircrafts.remove(i)
def remove_presets(self, name: str) -> None:
def remove_preset(self, name: str) -> None:
for pg in self.preset_groups:
if pg.name == name:
self.preset_groups.remove(pg)