diff --git a/game/ato/loadouts.py b/game/ato/loadouts.py index 9c187349..442be6b6 100644 --- a/game/ato/loadouts.py +++ b/game/ato/loadouts.py @@ -2,10 +2,13 @@ from __future__ import annotations import datetime from collections.abc import Iterable -from typing import Iterator, Mapping, Optional, TYPE_CHECKING +from typing import Iterator, Mapping, Optional, TYPE_CHECKING, Type + +from dcs.unittype import FlyingType from game.data.weapons import Pylon, Weapon, WeaponType from game.dcs.aircrafttype import AircraftType +from .flighttype import FlightType if TYPE_CHECKING: from .flight import Flight @@ -103,6 +106,10 @@ class Loadout: @classmethod def iter_for(cls, flight: Flight) -> Iterator[Loadout]: + return cls.iter_for_aircraft(flight.unit_type) + + @classmethod + def iter_for_aircraft(cls, aircraft: AircraftType) -> Iterator[Loadout]: # Dict of payload ID (numeric) to: # # { @@ -111,7 +118,7 @@ class Loadout: # {"CLSID": class ID, "num": pylon number} # "tasks": List (as a dict) of task IDs the payload is used by. # } - payloads = flight.unit_type.dcs_unit_type.load_payloads() + payloads = aircraft.dcs_unit_type.load_payloads() for payload in payloads.values(): name = payload["name"] pylons = payload["pylons"] @@ -122,9 +129,7 @@ class Loadout: ) @classmethod - def default_loadout_names_for(cls, flight: Flight) -> Iterator[str]: - from game.ato.flighttype import FlightType - + def default_loadout_names_for(cls, task: FlightType) -> Iterator[str]: # This is a list of mappings from the FlightType of a Flight to the type of # payload defined in the resources/payloads/UNIT_TYPE.lua file. A Flight has no # concept of a PyDCS task, so COMMON_OVERRIDE cannot be used here. This is used @@ -164,17 +169,25 @@ class Loadout: loadout_names[FlightType.DEAD].extend(loadout_names[FlightType.BAI]) # OCA/Runway falls back to Strike loadout_names[FlightType.OCA_RUNWAY].extend(loadout_names[FlightType.STRIKE]) - yield from loadout_names[flight.flight_type] + yield from loadout_names[task] @classmethod def default_for(cls, flight: Flight) -> Loadout: + return cls.default_for_task_and_aircraft( + flight.flight_type, flight.unit_type.dcs_unit_type + ) + + @classmethod + def default_for_task_and_aircraft( + cls, task: FlightType, dcs_unit_type: Type[FlyingType] + ) -> Loadout: # Iterate through each possible payload type for a given aircraft. # Some aircraft have custom loadouts that in aren't the standard set. - for name in cls.default_loadout_names_for(flight): + for name in cls.default_loadout_names_for(task): # This operation is cached, but must be called before load_by_name will # work. - flight.unit_type.dcs_unit_type.load_payloads() - payload = flight.unit_type.dcs_unit_type.loadout_by_name(name) + dcs_unit_type.load_payloads() + payload = dcs_unit_type.loadout_by_name(name) if payload is not None: return Loadout( name, diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py index 89da469e..cf537a20 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -4,7 +4,7 @@ import logging from dataclasses import dataclass from functools import cached_property from pathlib import Path -from typing import Any, Iterator, Optional, TYPE_CHECKING, Type, Dict +from typing import Any, Dict, Iterator, Optional, TYPE_CHECKING, Type import yaml from dcs.helicopters import helicopter_map @@ -315,7 +315,7 @@ class AircraftType(UnitType[Type[FlyingType]]): yield unit @staticmethod - def _each_unit_type() -> Iterator[Type[FlyingType]]: + def each_dcs_type() -> Iterator[Type[FlyingType]]: yield from helicopter_map.values() yield from plane_map.values() diff --git a/game/dcs/groundunittype.py b/game/dcs/groundunittype.py index c0995381..100c4546 100644 --- a/game/dcs/groundunittype.py +++ b/game/dcs/groundunittype.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging from dataclasses import dataclass from pathlib import Path -from typing import Any, Optional, Type, Iterator +from typing import Any, Iterator, Optional, Type import yaml from dcs.unittype import VehicleType @@ -76,7 +76,7 @@ class GroundUnitType(UnitType[Type[VehicleType]]): yield unit @staticmethod - def _each_unit_type() -> Iterator[Type[VehicleType]]: + def each_dcs_type() -> Iterator[Type[VehicleType]]: yield from vehicle_map.values() @classmethod diff --git a/game/dcs/shipunittype.py b/game/dcs/shipunittype.py index 0ae8e541..0b34f75b 100644 --- a/game/dcs/shipunittype.py +++ b/game/dcs/shipunittype.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging from dataclasses import dataclass from pathlib import Path -from typing import Type, Iterator +from typing import Iterator, Type import yaml from dcs.ships import ship_map @@ -32,7 +32,7 @@ class ShipUnitType(UnitType[Type[ShipType]]): yield unit @staticmethod - def _each_unit_type() -> Iterator[Type[ShipType]]: + def each_dcs_type() -> Iterator[Type[ShipType]]: yield from ship_map.values() @classmethod diff --git a/game/dcs/unittype.py b/game/dcs/unittype.py index 8ca3425b..8660d871 100644 --- a/game/dcs/unittype.py +++ b/game/dcs/unittype.py @@ -4,7 +4,7 @@ from abc import ABC from collections import defaultdict from dataclasses import dataclass from functools import cached_property -from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator +from typing import Any, ClassVar, Generic, Iterator, Type, TypeVar from dcs.unittype import UnitType as DcsUnitType @@ -52,7 +52,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]): raise NotImplementedError @staticmethod - def _each_unit_type() -> Iterator[DcsUnitTypeT]: + def each_dcs_type() -> Iterator[DcsUnitTypeT]: raise NotImplementedError @classmethod @@ -61,7 +61,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]): @classmethod def _load_all(cls) -> None: - for unit_type in cls._each_unit_type(): + for unit_type in cls.each_dcs_type(): for data in cls._each_variant_of(unit_type): cls.register(data) cls._loaded = True diff --git a/resources/tools/loadoutviewer.py b/resources/tools/loadoutviewer.py new file mode 100644 index 00000000..cb672ffe --- /dev/null +++ b/resources/tools/loadoutviewer.py @@ -0,0 +1,134 @@ +"""Command-line utility for displaying human readable loadout configurations.""" +import argparse +import sys +from collections.abc import Iterator +from pathlib import Path +from typing import Type + +from dcs.helicopters import helicopter_map +from dcs.planes import plane_map +from dcs.unittype import FlyingType + +from game import persistency +from game.ato import FlightType +from game.ato.loadouts import Loadout +from game.dcs.aircrafttype import AircraftType + +# TODO: Move this logic out of the UI. +from qt_ui import liberation_install +from qt_ui.main import inject_custom_payloads + + +def non_empty_loadouts_for( + aircraft: Type[FlyingType], +) -> Iterator[tuple[FlightType, Loadout]]: + for task in FlightType: + try: + loadout = Loadout.default_for_task_and_aircraft(task, aircraft) + except KeyError: + # Not all aircraft have a unitPayloads field. This should maybe be handled + # in pydcs, but I'm not sure about the cause. For now, just ignore the field + # since we can be less robust in optional tooling. + continue + + if loadout.name != "Empty": + yield task, loadout + + +def print_pylons(loadout: Loadout, prefix: str = "\t") -> None: + pylons = dict(sorted(loadout.pylons.items())) + for pylon_id, weapon in pylons.items(): + if weapon is not None: + print(f"{prefix}{pylon_id}: {weapon.name}") + + +def show_all_loadouts(aircraft: Type[FlyingType]) -> None: + loadouts = list(non_empty_loadouts_for(aircraft)) + if not loadouts: + return + + print(f"Loadouts for {aircraft.id}:") + for task, loadout in loadouts: + print(f"\t{task.value}: {loadout.name}") + print_pylons(loadout, prefix="\t\t") + + +def task_by_name(name: str) -> FlightType: + for task in FlightType: + if task.value == name: + return task + raise KeyError(f"No FlightType named {name}") + + +def show_single_loadout(aircraft: Type[FlyingType], task_name: str) -> None: + task = task_by_name(task_name) + try: + loadout = Loadout.default_for_task_and_aircraft(task, aircraft) + except KeyError: + # Not all aircraft have a unitPayloads field. This should maybe be handled + # in pydcs, but I'm not sure about the cause. For now, just ignore the field + # since we can be less robust in optional tooling. + return + if loadout.pylons: + print(f"{aircraft.id} {loadout.name}:") + print_pylons(loadout) + + +def show_loadouts_for(aircraft: Type[FlyingType], task_name: str | None) -> None: + if task_name is None: + show_all_loadouts(aircraft) + else: + show_single_loadout(aircraft, task_name) + + +def show_all_aircraft(task_name: str | None) -> None: + for aircraft in AircraftType.each_dcs_type(): + show_loadouts_for(aircraft, task_name) + + +def show_single_aircraft(aircraft_id: str, task_name: str | None) -> None: + try: + aircraft: Type[FlyingType] = plane_map[aircraft_id] + except KeyError: + aircraft = helicopter_map[aircraft_id] + show_loadouts_for(aircraft, task_name) + + +def main() -> None: + parser = argparse.ArgumentParser() + + parser.add_argument( + "--aircraft-id", + help=( + "ID of the aircraft to display loadouts for. By default all aircraft will " + + "be displayed." + ), + ) + + parser.add_argument( + "--task", + help=( + "Name of the mission type to display. By default loadouts for all mission " + + "types will be displayed." + ), + ) + + args = parser.parse_args() + + first_start = liberation_install.init() + if first_start: + sys.exit( + "Cannot view payloads without configuring DCS Liberation. Start the UI for " + "the first run configuration." + ) + + inject_custom_payloads(Path(persistency.base_path())) + + if args.aircraft_id is None: + show_all_aircraft(args.task) + else: + show_single_aircraft(args.aircraft_id, args.task) + + +if __name__ == "__main__": + main()