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

@@ -12,6 +12,7 @@ from dcs.helicopters import helicopter_map
from dcs.planes import plane_map
from dcs.unittype import FlyingType
from game.data.units import UnitClass
from game.dcs.unitproperty import UnitProperty
from game.dcs.unittype import UnitType
from game.radio.channels import (
@@ -180,19 +181,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer]
_by_name: ClassVar[dict[str, AircraftType]] = {}
_by_unit_type: ClassVar[dict[Type[FlyingType], list[AircraftType]]] = defaultdict(
list
)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@property
def flyable(self) -> bool:
return self.dcs_unit_type.flyable
@@ -309,35 +297,27 @@ class AircraftType(UnitType[Type[FlyingType]]):
state.update(updated.__dict__)
self.__dict__.update(state)
@classmethod
def register(cls, aircraft_type: AircraftType) -> None:
cls._by_name[aircraft_type.name] = aircraft_type
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
@classmethod
def named(cls, name: str) -> AircraftType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
unit = cls._by_name[name]
assert isinstance(unit, AircraftType)
return unit
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_unit_type[dcs_unit_type]
for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, AircraftType)
yield unit
@staticmethod
def _each_unit_type() -> Iterator[Type[FlyingType]]:
yield from helicopter_map.values()
yield from plane_map.values()
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@classmethod
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
@@ -417,4 +397,5 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_namer=radio_config.channel_namer,
kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False),
unit_class=UnitClass.Plane,
)

View File

@@ -1,65 +1,42 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Type, Optional, ClassVar, Iterator
from typing import Type, Optional, Iterator
import yaml
from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map
from game.data.groundunitclass import GroundUnitClass
from game.data.units import UnitClass
from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class GroundUnitType(UnitType[Type[VehicleType]]):
unit_class: Optional[GroundUnitClass]
spawn_weight: int
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
_by_unit_type: ClassVar[
dict[Type[VehicleType], list[GroundUnitType]]
] = defaultdict(list)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@classmethod
def register(cls, aircraft_type: GroundUnitType) -> None:
cls._by_name[aircraft_type.name] = aircraft_type
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
@classmethod
def named(cls, name: str) -> GroundUnitType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
unit = cls._by_name[name]
assert isinstance(unit, GroundUnitType)
return unit
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_unit_type[dcs_unit_type]
for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, GroundUnitType)
yield unit
@staticmethod
def _each_unit_type() -> Iterator[Type[VehicleType]]:
yield from vehicle_map.values()
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@classmethod
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
@@ -78,9 +55,8 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
introduction = "No data."
class_name = data.get("class")
unit_class: Optional[GroundUnitClass] = None
if class_name is not None:
unit_class = GroundUnitClass(class_name)
# TODO Exception handling for missing classes
unit_class = UnitClass(class_name) if class_name else UnitClass.Unknown
for variant in data.get("variants", [vehicle.id]):
yield GroundUnitType(

74
game/dcs/shipunittype.py Normal file
View File

@@ -0,0 +1,74 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Type, Optional, ClassVar, Iterator
import yaml
from dcs.ships import ship_map
from dcs.unittype import VehicleType, ShipType
from dcs.vehicles import vehicle_map
from game.data.units import UnitClass
from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class ShipUnitType(UnitType[Type[ShipType]]):
@classmethod
def named(cls, name: str) -> ShipUnitType:
if not cls._loaded:
cls._load_all()
unit = cls._by_name[name]
assert isinstance(unit, ShipUnitType)
return unit
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[ShipType]) -> Iterator[ShipUnitType]:
if not cls._loaded:
cls._load_all()
for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, ShipUnitType)
yield unit
@staticmethod
def _each_unit_type() -> Iterator[Type[ShipType]]:
yield from ship_map.values()
@classmethod
def _each_variant_of(cls, ship: Type[ShipType]) -> Iterator[ShipUnitType]:
data_path = Path("resources/units/ships") / f"{ship.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {ship.id}; it will not be available")
return
with data_path.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
try:
introduction = data["introduced"]
if introduction is None:
introduction = "N/A"
except KeyError:
introduction = "No data."
class_name = data.get("class")
unit_class = UnitClass(class_name)
for variant in data.get("variants", [ship.id]):
yield ShipUnitType(
dcs_unit_type=ship,
unit_class=unit_class,
name=variant,
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=data.get("price", 1),
)

157
game/dcs/unitgroup.py Normal file
View File

@@ -0,0 +1,157 @@
from __future__ import annotations
import copy
import itertools
import random
from dataclasses import dataclass, field
from pathlib import Path
from typing import ClassVar, TYPE_CHECKING, Any, Iterator
import yaml
from game.data.groups import GroupRole, GroupTask
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unittype import UnitType
from game.point_with_heading import PointWithHeading
from gen.templates import GroundObjectTemplate
if TYPE_CHECKING:
from game import Game
from game.factions.faction import Faction
from game.theater import TheaterGroundObject, ControlPoint
@dataclass
class UnitGroup:
name: str
ground_units: list[GroundUnitType]
ship_units: list[ShipUnitType]
statics: list[str]
role: GroupRole
tasks: list[GroupTask] = field(default_factory=list)
template_names: list[str] = field(default_factory=list)
_by_name: ClassVar[dict[str, UnitGroup]] = {}
_by_role: ClassVar[dict[GroupRole, list[UnitGroup]]] = {}
_loaded: bool = False
_templates: list[GroundObjectTemplate] = field(default_factory=list)
def __str__(self) -> str:
return self.name
def update_from_unit_group(self, unit_group: UnitGroup) -> None:
# Update tasking and templates
self.tasks.extend([task for task in unit_group.tasks if task not in self.tasks])
self._templates.extend(
[
template
for template in unit_group.templates
if template not in self.templates
]
)
@property
def templates(self) -> list[GroundObjectTemplate]:
return self._templates
def add_template(self, faction_template: GroundObjectTemplate) -> None:
template = copy.deepcopy(faction_template)
updated_groups = []
for group in template.groups:
unit_types = list(
itertools.chain(
[u.dcs_id for u in self.ground_units if group.can_use_unit(u)],
[s.dcs_id for s in self.ship_units if group.can_use_unit(s)],
[s for s in self.statics if group.can_use_unit_type(s)],
)
)
if unit_types:
group.set_possible_types(unit_types)
updated_groups.append(group)
template.groups = updated_groups
self._templates.append(template)
def load_templates(self, faction: Faction) -> None:
self._templates = []
if self.template_names:
# Preferred templates
for template_name in self.template_names:
template = faction.templates.by_name(template_name)
if template:
self.add_template(template)
if not self._templates:
# Find all matching templates if no preferred set or available
for template in list(
faction.templates.for_role_and_tasks(self.role, self.tasks)
):
if any(self.has_unit_type(unit) for unit in template.units):
self.add_template(template)
def set_templates(self, templates: list[GroundObjectTemplate]) -> None:
self._templates = templates
def has_unit_type(self, unit_type: UnitType[Any]) -> bool:
return unit_type in self.ground_units or unit_type in self.ship_units
@property
def unit_types(self) -> Iterator[str]:
for unit in self.ground_units:
yield unit.dcs_id
for ship in self.ship_units:
yield ship.dcs_id
for static in self.statics:
yield static
@classmethod
def named(cls, name: str) -> UnitGroup:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
def generate(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
game: Game,
) -> TheaterGroundObject:
template = random.choice(self.templates)
return template.generate(name, position, control_point, game)
@classmethod
def _load_all(cls) -> None:
for file in Path("resources/units/unit_groups").glob("*.yaml"):
if not file.is_file():
continue
with file.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
group_role = GroupRole(data.get("role"))
group_tasks = [GroupTask(n) for n in data.get("tasks", [])]
ground_units = [
GroundUnitType.named(n) for n in data.get("ground_units", [])
]
ship_units = [ShipUnitType.named(n) for n in data.get("ship_units", [])]
unit_group = UnitGroup(
name=data.get("name"),
ground_units=ground_units,
ship_units=ship_units,
statics=data.get("statics", []),
role=group_role,
tasks=group_tasks,
template_names=data.get("templates", []),
)
cls._by_name[unit_group.name] = unit_group
if group_role in cls._by_role:
cls._by_role[group_role].append(unit_group)
else:
cls._by_role[group_role] = [unit_group]
cls._loaded = True

View File

@@ -1,14 +1,20 @@
from __future__ import annotations
from abc import ABC
from collections import defaultdict
from dataclasses import dataclass
from functools import cached_property
from typing import TypeVar, Generic, Type
from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator, Optional
from dcs.unittype import UnitType as DcsUnitType
from game.data.units import UnitClass
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
@dataclass(frozen=True)
class UnitType(Generic[DcsUnitTypeT]):
class UnitType(ABC, Generic[DcsUnitTypeT]):
dcs_unit_type: DcsUnitTypeT
name: str
description: str
@@ -17,10 +23,47 @@ class UnitType(Generic[DcsUnitTypeT]):
manufacturer: str
role: str
price: int
unit_class: UnitClass
_by_name: ClassVar[dict[str, UnitType[Any]]] = {}
_by_unit_type: ClassVar[dict[DcsUnitTypeT, list[UnitType[Any]]]] = defaultdict(list)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@classmethod
def register(cls, unit_type: UnitType[Any]) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@classmethod
def named(cls, name: str) -> UnitType[Any]:
raise NotImplementedError
@classmethod
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
raise NotImplementedError
@staticmethod
def _each_unit_type() -> Iterator[DcsUnitTypeT]:
raise NotImplementedError
@classmethod
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
raise NotImplementedError
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@cached_property
def eplrs_capable(self) -> bool:
return getattr(self.dcs_unit_type, "eplrs", False)