dcs-retribution/game/dcs/groundunittype.py
Dan Albert 81554c510e
Fix merged classvars in UnitType descendants.
```
>>> class Foo:
...     bar = 0
...     @classmethod
...     def set_bar(cls, v):
...             cls.bar = v
...
>>> class Bar(Foo):
...     ...
...
>>> Bar.set_bar(1)
>>> Bar.bar
1
>>> Foo.bar
0
>>> class Foo:
...     bar = {}
...     @classmethod
...     def add(cls, k, v):
...             cls.bar[k] = v
...
>>> class Bar(Foo):
...     pass
...
>>> Bar.add(0, 1)
>>> Bar.bar
{0: 1}
>>> Foo.bar
{0: 1}
```

The collections are copied by reference into the descendants, whereas
_loaded is copied by value, so that one can stay. Before this patch,
every subtype was loading because _loaded was set per subclass, but they
were all registering with a common collection defined by UnitType rather
than their own class.
2023-05-14 14:08:37 +02:00

133 lines
4.8 KiB
Python

from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Any, ClassVar, Iterator, Optional, Type
import yaml
from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map
from game.data.units import UnitClass
from game.dcs.unittype import UnitType
@dataclass
class SkynetProperties:
can_engage_harm: Optional[str] = None
can_engage_air_weapon: Optional[str] = None
go_live_range_in_percent: Optional[str] = None
engagement_zone: Optional[str] = None
autonomous_behaviour: Optional[str] = None
harm_detection_chance: Optional[str] = None
@classmethod
def from_data(cls, data: dict[str, Any]) -> SkynetProperties:
props = SkynetProperties()
if "can_engage_harm" in data:
props.can_engage_harm = str(data["can_engage_harm"]).lower()
if "can_engage_air_weapon" in data:
props.can_engage_air_weapon = str(data["can_engage_air_weapon"]).lower()
if "go_live_range_in_percent" in data:
props.go_live_range_in_percent = str(data["go_live_range_in_percent"])
if "engagement_zone" in data:
props.engagement_zone = str(data["engagement_zone"])
if "autonomous_behaviour" in data:
props.autonomous_behaviour = str(data["autonomous_behaviour"])
if "harm_detection_chance" in data:
props.harm_detection_chance = str(data["harm_detection_chance"])
return props
def to_dict(self) -> dict[str, str]:
properties: dict[str, str] = {}
for key, value in self.__dict__.items():
if value is not None:
properties[key] = value
return properties
def __hash__(self) -> int:
return hash(id(self))
@dataclass(frozen=True)
class GroundUnitType(UnitType[Type[VehicleType]]):
spawn_weight: int
skynet_properties: SkynetProperties
# Defines if we should place the ground unit with an inverted heading.
# Some units like few Launchers have to be placed backwards to be able to fire.
reversed_heading: bool = False
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
_by_unit_type: ClassVar[
dict[type[VehicleType], list[GroundUnitType]]
] = defaultdict(list)
@classmethod
def register(cls, unit_type: GroundUnitType) -> 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) -> GroundUnitType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
@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]
@staticmethod
def each_dcs_type() -> Iterator[Type[VehicleType]]:
yield from vehicle_map.values()
@classmethod
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {vehicle.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")
if class_name is None:
logging.warning(f"{vehicle.id} has no class")
unit_class = UnitClass.UNKNOWN
else:
unit_class = UnitClass(class_name)
for variant in data.get("variants", [vehicle.id]):
yield GroundUnitType(
dcs_unit_type=vehicle,
unit_class=unit_class,
spawn_weight=data.get("spawn_weight", 0),
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),
skynet_properties=SkynetProperties.from_data(
data.get("skynet_properties", {})
),
reversed_heading=data.get("reversed_heading", False),
)