Decoupling and generalization of templates

Improvement for factions and templates which will allow decoupling of the templates from the actual units
- Implement UnitGroup class which matches unit_types and possible templates as the needed abstraction layer for decoupling.
- Refactor UnitType, Add ShipUnitType and all ships we currently use
- Remove serialized template.json and migrated to multiple yaml templates (one for each template) and multiple .miz
- Reorganized a lot of templates and started with generalization of many types (AAA, Flak, SHORAD, Navy)
- Fixed a lot of bugs from the previous reworks (group name generation, strike targets...)
- Reorganized the faction file completly. removed redundant lists, added presets for complex groups / families of units like sams
- Reworked the building template handling. Some templates are unused like "village"
- Reworked how groups from templates can be merged again for the dcs group creation (e.g. the skynet plugin requires them to be in the same group)
- Allow to define alternative tasks
This commit is contained in:
RndName
2022-01-29 00:42:58 +01:00
parent daf4704fe7
commit 60c8c80480
27 changed files with 1481 additions and 958 deletions

View File

@@ -3,12 +3,13 @@ from __future__ import annotations
import copy
import itertools
import logging
import random
from dataclasses import dataclass, field
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
import dcs
from dcs.countries import country_dict
from dcs.unittype import ShipType, UnitType
from dcs.unittype import ShipType
from game.data.building_data import (
WW2_ALLIES_BUILDINGS,
@@ -22,10 +23,19 @@ from game.data.doctrine import (
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
)
from game.data.groundunitclass import GroundUnitClass
from game.data.units import UnitClass
from game.data.groups import GroupRole, GroupTask
from game import db
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from gen.templates import GroundObjectTemplates, TemplateCategory
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unitgroup import UnitGroup
from game.dcs.unittype import UnitType
from gen.templates import (
GroundObjectTemplates,
GroundObjectTemplate,
GroupTemplate,
)
if TYPE_CHECKING:
from game.theater.start_generator import ModSettings
@@ -70,50 +80,26 @@ class Faction:
# Logistics units used
logistics_units: List[GroundUnitType] = field(default_factory=list)
# Possible SAMS site generators for this faction
air_defenses: List[str] = field(default_factory=list)
# Possible Air Defence units, Like EWRs
air_defense_units: List[GroundUnitType] = field(default_factory=list)
# Possible EWR generators for this faction.
ewrs: List[GroundUnitType] = field(default_factory=list)
# A list of all supported sets of units
preset_groups: list[UnitGroup] = field(default_factory=list)
# Possible Missile site generators for this faction
missiles: List[str] = field(default_factory=list)
# Possible costal site generators for this faction
coastal_defenses: List[str] = field(default_factory=list)
missiles: List[GroundUnitType] = field(default_factory=list)
# Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict)
# possible aircraft carrier units
aircraft_carrier: List[Type[ShipType]] = field(default_factory=list)
# possible helicopter carrier units
helicopter_carrier: List[Type[ShipType]] = field(default_factory=list)
# Possible carrier names
carrier_names: List[str] = field(default_factory=list)
# Possible helicopter carrier names
helicopter_carrier_names: List[str] = field(default_factory=list)
# Navy group generators
navy_generators: List[str] = field(default_factory=list)
# Available destroyers
destroyers: List[Type[ShipType]] = field(default_factory=list)
# Available cruisers
cruisers: List[Type[ShipType]] = field(default_factory=list)
# How many navy group should we try to generate per CP on startup for this faction
navy_group_count: int = field(default=1)
# How many missiles group should we try to generate per CP on startup for this faction
missiles_group_count: int = field(default=1)
# How many coastal group should we try to generate per CP on startup for this faction
coastal_group_count: int = field(default=1)
# Available Naval Units
naval_units: List[ShipUnitType] = field(default_factory=list)
# Whether this faction has JTAC access
has_jtac: bool = field(default=False)
@@ -124,7 +110,7 @@ class Faction:
# doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
# List of available buildings for this faction
# List of available building templates for this faction
building_set: List[str] = field(default_factory=list)
# List of default livery overrides
@@ -142,64 +128,183 @@ class Faction:
# 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)
def has_access_to_unit_type(self, unit_type: str) -> bool:
# Supports all GroundUnit lists and AirDefenses
for unit in self.ground_units:
if unit_type == unit.dcs_id:
return True
return unit_type in self.air_defenses
@property
def accessible_units(self) -> Iterator[UnitType[Any]]:
yield from self._accessible_units
def has_access_to_unit_class(self, unit_class: GroundUnitClass) -> bool:
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
if vehicle.unit_class is unit_class:
return True
@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):
return True
# Statics
if db.static_type_from_name(unit_type) is not None:
# TODO Improve the statics checking
return True
return False
def load_templates(self, all_templates: GroundObjectTemplates) -> None:
# This loads all faction possible sam templates and the default ones
# For legacy reasons this allows to check for template names. This can be
# improved in the future to have more control about the possible Templates.
# For example it can be possible to define the unit_types and check if all
# requirements for the template are fulfilled.
for category, template in all_templates.templates:
if (
(
category == TemplateCategory.AirDefence
and (
# Check if faction has the template name or ALL required
# unit_types in the list air_defenses. For legacy reasons this
# allows both and also the EWR template
template.name in self.air_defenses
or all(
self.has_access_to_unit_type(required_unit)
for required_unit in template.required_units
)
or template.template_type == "EWR"
)
)
or template.name in self.navy_generators
or template.name in self.missiles
or template.name in self.coastal_defenses
or (
template.template_type
in self.building_set + ["fob", "ammo", "factory"]
)
or (template.template_type == "carrier" and self.aircraft_carrier)
or (template.template_type == "lha" and self.helicopter_carrier)
or category == TemplateCategory.Armor
):
# 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)
# Initialize all randomizers
for group_template in faction_template.groups:
if group_template.randomizer:
group_template.randomizer.init_randomization_for_faction(self)
self.templates.add_template(category, faction_template)
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 = []
all_units: Iterator[UnitType[Any]] = itertools.chain(
self.ground_units,
self.infantry_units,
self.air_defense_units,
self.naval_units,
self.missiles,
(
ground_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 all_units:
if unit not in self._accessible_units:
self._accessible_units.append(unit)
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,
)
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}")
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@@ -240,34 +345,30 @@ class Faction:
faction.logistics_units = [
GroundUnitType.named(n) for n in json.get("logistics_units", [])
]
faction.ewrs = [GroundUnitType.named(n) for n in json.get("ewrs", [])]
faction.air_defense_units = [
GroundUnitType.named(n) for n in json.get("air_defense_units", [])
]
faction.missiles = [GroundUnitType.named(n) for n in json.get("missiles", [])]
faction.air_defenses = json.get("air_defenses", [])
# Compatibility for older factions. All air defenses now belong to a
# single group and the generator decides what belongs where.
faction.air_defenses.extend(json.get("sams", []))
faction.air_defenses.extend(json.get("shorads", []))
faction.preset_groups = [
UnitGroup.named(n) for n in json.get("preset_groups", [])
]
faction.missiles = json.get("missiles", [])
faction.coastal_defenses = json.get("coastal_defenses", [])
faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
faction.navy_generators = json.get("navy_generators", [])
faction.aircraft_carrier = load_all_ships(json.get("aircraft_carrier", []))
faction.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", []))
faction.destroyers = load_all_ships(json.get("destroyers", []))
faction.cruisers = load_all_ships(json.get("cruisers", []))
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:
faction.jtac_unit = AircraftType.named(jtac_name)
else:
faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1))
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
# Load doctrine
doctrine = json.get("doctrine", "modern")
@@ -304,6 +405,7 @@ class Faction:
# Templates
faction.templates = GroundObjectTemplates()
return faction
@property
@@ -312,14 +414,49 @@ class Faction:
yield from self.frontline_units
yield from self.logistics_units
def infantry_with_class(
self, unit_class: GroundUnitClass
) -> Iterator[GroundUnitType]:
def infantry_with_class(self, unit_class: UnitClass) -> Iterator[GroundUnitType]:
for unit in self.infantry_units:
if unit.unit_class is unit_class:
yield unit
def apply_mod_settings(self, mod_settings: ModSettings) -> Faction:
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:
# aircraft
if not mod_settings.a4_skyhawk:
self.remove_aircraft("A-4E-C")
@@ -379,24 +516,23 @@ class Faction:
self.remove_vehicle("KORNET")
# high digit sams
if not mod_settings.high_digit_sams:
self.remove_air_defenses("SA-10B/S-300PS Battery")
self.remove_air_defenses("SA-12/S-300V Battery")
self.remove_air_defenses("SA-20/S-300PMU-1 Battery")
self.remove_air_defenses("SA-20B/S-300PMU-2 Battery")
self.remove_air_defenses("SA-23/S-300VM Battery")
self.remove_air_defenses("SA-17 Grizzly Battery")
self.remove_air_defenses("KS-19 AAA Site")
return self
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")
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_air_defenses(self, name: str) -> None:
for i in self.air_defenses:
if i == name:
self.air_defenses.remove(i)
def remove_presets(self, name: str) -> None:
for pg in self.preset_groups:
if pg.name == name:
self.preset_groups.remove(pg)
def remove_vehicle(self, name: str) -> None:
for i in self.frontline_units: