mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
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:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
74
game/dcs/shipunittype.py
Normal 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
157
game/dcs/unitgroup.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user