Fix unit variants to actually allow variance.

This was always the intent, but apparently it wasn't implemented
correctly. All properties of the unit type can now be overridden per
variant.
This commit is contained in:
Dan Albert 2023-08-09 22:18:44 -07:00
parent 1f3eee90f1
commit 58d8203c83
5 changed files with 120 additions and 108 deletions

View File

@ -24,6 +24,7 @@ Saves from 8.x are not compatible with 9.0.0.
* **[Mission Generation]** Restored previous AI behavior for anti-ship missions. A DCS update caused only a single aircraft in a flight to attack. The full flight will now attack like they used to. * **[Mission Generation]** Restored previous AI behavior for anti-ship missions. A DCS update caused only a single aircraft in a flight to attack. The full flight will now attack like they used to.
* **[Mission Generation]** Fix generation of OCA Runway missions to allow LGBs to be used. * **[Mission Generation]** Fix generation of OCA Runway missions to allow LGBs to be used.
* **[Mission Generation]** Fixed AI flights flying far too slowly toward NAV points. * **[Mission Generation]** Fixed AI flights flying far too slowly toward NAV points.
* **[Modding]** Unit variants can now actually override base unit type properties.
* **[New Game Wizard]** Factions are reset to default after clicking "Back" to Theater Configuration screen. * **[New Game Wizard]** Factions are reset to default after clicking "Back" to Theater Configuration screen.
* **[Plugins]** Fixed Lua errors in Skynet plugin that would occur whenever one coalition had no IADS nodes. * **[Plugins]** Fixed Lua errors in Skynet plugin that would occur whenever one coalition had no IADS nodes.
* **[UI]** Fixed deleting waypoints in custom flight plans deleting the wrong waypoint. * **[UI]** Fixed deleting waypoints in custom flight plans deleting the wrong waypoint.

View File

@ -7,7 +7,6 @@ from functools import cache, cached_property
from pathlib import Path from pathlib import Path
from typing import Any, ClassVar, Dict, Iterator, Optional, TYPE_CHECKING, Type from typing import Any, ClassVar, Dict, Iterator, Optional, TYPE_CHECKING, Type
import yaml
from dcs.helicopters import helicopter_map from dcs.helicopters import helicopter_map
from dcs.planes import plane_map from dcs.planes import plane_map
from dcs.unitpropertydescription import UnitPropertyDescription from dcs.unitpropertydescription import UnitPropertyDescription
@ -395,11 +394,11 @@ class AircraftType(UnitType[Type[FlyingType]]):
@staticmethod @staticmethod
def _set_props_overrides( def _set_props_overrides(
config: Dict[str, Any], aircraft: Type[FlyingType], data_path: Path config: Dict[str, Any], aircraft: Type[FlyingType]
) -> None: ) -> None:
if aircraft.property_defaults is None: if aircraft.property_defaults is None:
logging.warning( logging.warning(
f"'{data_path.name}' attempted to set default prop that does not exist." f"'{aircraft.id}' attempted to set default prop that does not exist."
) )
else: else:
for k in config: for k in config:
@ -407,25 +406,23 @@ class AircraftType(UnitType[Type[FlyingType]]):
aircraft.property_defaults[k] = config[k] aircraft.property_defaults[k] = config[k]
else: else:
logging.warning( logging.warning(
f"'{data_path.name}' attempted to set default prop '{k}' that does not exist" f"'{aircraft.id}' attempted to set default prop '{k}' that does not exist"
) )
@classmethod @classmethod
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]: def _data_directory(cls) -> Path:
return Path("resources/units/aircraft")
@classmethod
def _variant_from_dict(
cls, aircraft: Type[FlyingType], variant_id: str, data: dict[str, Any]
) -> AircraftType:
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {aircraft.id}; it will not be available")
return
with data_path.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
try: try:
price = data["price"] price = data["price"]
except KeyError as ex: except KeyError as ex:
raise KeyError(f"Missing required price field: {data_path}") from ex raise KeyError(f"Missing required price field") from ex
radio_config = RadioConfig.from_data(data.get("radios", {})) radio_config = RadioConfig.from_data(data.get("radios", {}))
patrol_config = PatrolConfig.from_data(data.get("patrol", {})) patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
@ -468,50 +465,49 @@ class AircraftType(UnitType[Type[FlyingType]]):
prop_overrides = data.get("default_overrides") prop_overrides = data.get("default_overrides")
if prop_overrides is not None: if prop_overrides is not None:
cls._set_props_overrides(prop_overrides, aircraft, data_path) cls._set_props_overrides(prop_overrides, aircraft)
task_priorities: dict[FlightType, int] = {} task_priorities: dict[FlightType, int] = {}
for task_name, priority in data.get("tasks", {}).items(): for task_name, priority in data.get("tasks", {}).items():
task_priorities[FlightType(task_name)] = priority task_priorities[FlightType(task_name)] = priority
for variant in data.get("variants", [aircraft.id]): return AircraftType(
yield AircraftType( dcs_unit_type=aircraft,
dcs_unit_type=aircraft, variant_id=variant_id,
variant_id=variant, description=data.get(
description=data.get( "description",
"description", f"No data. <a href=\"https://google.com/search?q=DCS+{variant_id.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant_id}</span></a>",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>", ),
), year_introduced=introduction,
year_introduced=introduction, country_of_origin=data.get("origin", "No data."),
country_of_origin=data.get("origin", "No data."), manufacturer=data.get("manufacturer", "No data."),
manufacturer=data.get("manufacturer", "No data."), role=data.get("role", "No data."),
role=data.get("role", "No data."), price=price,
price=price, carrier_capable=data.get("carrier_capable", False),
carrier_capable=data.get("carrier_capable", False), lha_capable=data.get("lha_capable", False),
lha_capable=data.get("lha_capable", False), always_keeps_gun=data.get("always_keeps_gun", False),
always_keeps_gun=data.get("always_keeps_gun", False), gunfighter=data.get("gunfighter", False),
gunfighter=data.get("gunfighter", False), max_group_size=data.get("max_group_size", aircraft.group_size_max),
max_group_size=data.get("max_group_size", aircraft.group_size_max), patrol_altitude=patrol_config.altitude,
patrol_altitude=patrol_config.altitude, patrol_speed=patrol_config.speed,
patrol_speed=patrol_config.speed, max_mission_range=mission_range,
max_mission_range=mission_range, fuel_consumption=fuel_consumption,
fuel_consumption=fuel_consumption, default_livery=data.get("default_livery"),
default_livery=data.get("default_livery"), intra_flight_radio=radio_config.intra_flight,
intra_flight_radio=radio_config.intra_flight, channel_allocator=radio_config.channel_allocator,
channel_allocator=radio_config.channel_allocator, channel_namer=radio_config.channel_namer,
channel_namer=radio_config.channel_namer, kneeboard_units=units,
kneeboard_units=units, utc_kneeboard=data.get("utc_kneeboard", False),
utc_kneeboard=data.get("utc_kneeboard", False), unit_class=unit_class,
unit_class=unit_class, cabin_size=data.get("cabin_size", 10 if aircraft.helicopter else 0),
cabin_size=data.get("cabin_size", 10 if aircraft.helicopter else 0), can_carry_crates=data.get("can_carry_crates", aircraft.helicopter),
can_carry_crates=data.get("can_carry_crates", aircraft.helicopter), task_priorities=task_priorities,
task_priorities=task_priorities, has_built_in_target_pod=data.get("has_built_in_target_pod", False),
has_built_in_target_pod=data.get("has_built_in_target_pod", False), laser_code_configs=[
laser_code_configs=[ LaserCodeConfig.from_yaml(d) for d in data.get("laser_codes", [])
LaserCodeConfig.from_yaml(d) for d in data.get("laser_codes", []) ],
], use_f15e_waypoint_names=data.get("use_f15e_waypoint_names", False),
use_f15e_waypoint_names=data.get("use_f15e_waypoint_names", False), )
)
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self.variant_id) return hash(self.variant_id)

View File

@ -6,7 +6,6 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, ClassVar, Iterator, Optional, Type from typing import Any, ClassVar, Iterator, Optional, Type
import yaml
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
@ -99,15 +98,13 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
yield from vehicle_map.values() yield from vehicle_map.values()
@classmethod @classmethod
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]: def _data_directory(cls) -> Path:
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml" return Path("resources/units/ground_units")
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)
@classmethod
def _variant_from_dict(
cls, vehicle: Type[VehicleType], variant_id: str, data: dict[str, Any]
) -> GroundUnitType:
try: try:
introduction = data["introduced"] introduction = data["introduced"]
if introduction is None: if introduction is None:
@ -122,23 +119,22 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
else: else:
unit_class = UnitClass(class_name) unit_class = UnitClass(class_name)
for variant in data.get("variants", [vehicle.id]): return GroundUnitType(
yield GroundUnitType( dcs_unit_type=vehicle,
dcs_unit_type=vehicle, unit_class=unit_class,
unit_class=unit_class, spawn_weight=data.get("spawn_weight", 0),
spawn_weight=data.get("spawn_weight", 0), variant_id=variant_id,
variant_id=variant, description=data.get(
description=data.get( "description",
"description", f"No data. <a href=\"https://google.com/search?q=DCS+{variant_id.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant_id}</span></a>",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>", ),
), year_introduced=introduction,
year_introduced=introduction, country_of_origin=data.get("origin", "No data."),
country_of_origin=data.get("origin", "No data."), manufacturer=data.get("manufacturer", "No data."),
manufacturer=data.get("manufacturer", "No data."), role=data.get("role", "No data."),
role=data.get("role", "No data."), price=data.get("price", 1),
price=data.get("price", 1), skynet_properties=SkynetProperties.from_data(
skynet_properties=SkynetProperties.from_data( data.get("skynet_properties", {})
data.get("skynet_properties", {}) ),
), reversed_heading=data.get("reversed_heading", False),
reversed_heading=data.get("reversed_heading", False), )
)

View File

@ -1,12 +1,10 @@
from __future__ import annotations from __future__ import annotations
import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import ClassVar, Iterator, Type, Any from typing import ClassVar, Iterator, Type, Any
import yaml
from dcs.ships import ship_map from dcs.ships import ship_map
from dcs.unittype import ShipType from dcs.unittype import ShipType
@ -55,15 +53,13 @@ class ShipUnitType(UnitType[Type[ShipType]]):
yield from ship_map.values() yield from ship_map.values()
@classmethod @classmethod
def _each_variant_of(cls, ship: Type[ShipType]) -> Iterator[ShipUnitType]: def _data_directory(cls) -> Path:
data_path = Path("resources/units/ships") / f"{ship.id}.yaml" return Path("resources/units/ships")
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)
@classmethod
def _variant_from_dict(
cls, ship: Type[ShipType], variant_id: str, data: dict[str, Any]
) -> ShipUnitType:
try: try:
introduction = data["introduced"] introduction = data["introduced"]
if introduction is None: if introduction is None:
@ -74,18 +70,17 @@ class ShipUnitType(UnitType[Type[ShipType]]):
class_name = data.get("class") class_name = data.get("class")
unit_class = UnitClass(class_name) unit_class = UnitClass(class_name)
for variant in data.get("variants", [ship.id]): return ShipUnitType(
yield ShipUnitType( dcs_unit_type=ship,
dcs_unit_type=ship, unit_class=unit_class,
unit_class=unit_class, variant_id=variant_id,
variant_id=variant, description=data.get(
description=data.get( "description",
"description", f"No data. <a href=\"https://google.com/search?q=DCS+{variant_id.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant_id}</span></a>",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>", ),
), year_introduced=introduction,
year_introduced=introduction, country_of_origin=data.get("origin", "No data."),
country_of_origin=data.get("origin", "No data."), manufacturer=data.get("manufacturer", "No data."),
manufacturer=data.get("manufacturer", "No data."), role=data.get("role", "No data."),
role=data.get("role", "No data."), price=data["price"],
price=data.get("price"), )
)

View File

@ -1,10 +1,13 @@
from __future__ import annotations from __future__ import annotations
import logging
from abc import ABC from abc import ABC
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
from typing import ClassVar, Generic, Iterator, Self, Type, TypeVar from pathlib import Path
from typing import ClassVar, Generic, Iterator, Self, Type, TypeVar, Any
import yaml
from dcs.unittype import UnitType as DcsUnitType from dcs.unittype import UnitType as DcsUnitType
from game.data.units import UnitClass from game.data.units import UnitClass
@ -49,8 +52,29 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
def each_dcs_type() -> Iterator[DcsUnitTypeT]: def each_dcs_type() -> Iterator[DcsUnitTypeT]:
raise NotImplementedError raise NotImplementedError
@classmethod
def _data_directory(cls) -> Path:
raise NotImplementedError
@classmethod @classmethod
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[Self]: def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[Self]:
data_path = cls._data_directory() / f"{unit.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {unit.id}; it will not be available")
return
with data_path.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
for variant_id, variant_data in data.get("variants", {unit.id: {}}).items():
if variant_data is None:
variant_data = {}
yield cls._variant_from_dict(unit, variant_id, data | variant_data)
@classmethod
def _variant_from_dict(
cls, dcs_unit_type: DcsUnitTypeT, variant_id: str, data: dict[str, Any]
) -> Self:
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod