Add a CLI tool for viewing default loadouts.

This commit is contained in:
Dan Albert 2022-05-29 15:23:21 -07:00
parent c5efc908de
commit 22c3d4ebc5
6 changed files with 165 additions and 18 deletions

View File

@ -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,

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()