Allow per pilot loadouts and properties.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3092.
This commit is contained in:
Dan Albert
2023-07-20 21:49:21 -07:00
committed by Raffson
parent 8e670e1a3c
commit 485229b92f
27 changed files with 475 additions and 115 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import uuid
from collections.abc import Iterator
from datetime import datetime, timedelta
from typing import Any, List, Optional, TYPE_CHECKING
@@ -9,10 +10,11 @@ from dcs.planes import C_101CC, C_101EB, Su_33, FA_18C_hornet
from game.dcs.aircrafttype import AircraftType
from pydcs_extensions.hercules.hercules import Hercules
from .flightmembers import FlightMembers
from .flightroster import FlightRoster
from .flightstate import FlightState, Navigating, Uninitialized
from .flightstate.killed import Killed
from .loadouts import Loadout, Weapon
from .loadouts import Weapon
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
from ..radio.TacanContainer import TacanContainer
from ..radio.radios import RadioFrequency
@@ -31,6 +33,8 @@ if TYPE_CHECKING:
from game.squadrons import Squadron, Pilot
from game.theater import ControlPoint
from game.transfers import TransferOrder
from game.data.weapons import WeaponType
from .flightmember import FlightMember
from .flightplans.flightplan import FlightPlan
from .flighttype import FlightType
from .flightwaypoint import FlightWaypoint
@@ -61,21 +65,16 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
self.package = package
self.coalition = squadron.coalition
self.squadron = squadron
self.flight_type = flight_type
if claim_inv:
self.squadron.claim_inventory(count)
if roster is None:
self.roster = FlightRoster(self.squadron, initial_size=count)
self.roster = FlightMembers(self, initial_size=count)
else:
self.roster = roster
self.roster = FlightMembers.from_roster(self, roster)
self.divert = divert
self.flight_type = flight_type
self.loadout = Loadout.default_for(self)
if self.squadron.aircraft.name == "F-15I Ra'am":
self.loadout.pylons[16] = Weapon.with_clsid(
"{IDF_MODS_PROJECT_F-15I_Raam_Dome}"
)
self.start_type = start_type
self.use_custom_loadout = False
self.custom_name = custom_name
self.group_id: int = 0
@@ -85,6 +84,7 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
self.tcn_name = callsign
self.initialize_fuel()
self.use_same_loadout_for_all_members = True
# Only used by transport missions.
self.cargo = cargo
@@ -138,7 +138,11 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
def __setstate__(self, state: dict[str, Any]) -> None:
state["state"] = Uninitialized(self, state["squadron"].settings)
if "use_same_loadout_for_all_members" not in state:
state["use_same_loadout_for_all_members"] = True
self.__dict__.update(state)
if isinstance(self.roster, FlightRoster):
self.roster = FlightMembers.from_roster(self, self.roster)
@property
def blue(self) -> bool:
@@ -206,6 +210,9 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
def missing_pilots(self) -> int:
return self.roster.missing_pilots
def iter_members(self) -> Iterator[FlightMember]:
yield from self.roster.members
def set_flight_type(self, var: FlightType) -> None:
self.flight_type = var
@@ -235,6 +242,11 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
elif self.departure.cptype.name in ["FARP", "FOB"] and not self.is_helo:
self.fuel = unit_type.fuel_max * 0.75
def any_member_has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
return any(
m.loadout.has_weapon_of_type(weapon_type) for m in self.iter_members()
)
def __repr__(self) -> str:
return self.__str__()
@@ -294,7 +306,7 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
Killed(self.state.estimate_position(), self, self.squadron.settings)
)
events.update_flight(self)
for pilot in self.roster.pilots:
for pilot in self.roster.iter_pilots():
if pilot is not None:
results.kill_pilot(self, pilot)

22
game/ato/flightmember.py Normal file
View File

@@ -0,0 +1,22 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from game.ato.loadouts import Loadout
if TYPE_CHECKING:
from game.squadrons import Pilot
class FlightMember:
def __init__(self, pilot: Pilot | None, loadout: Loadout) -> None:
self.pilot = pilot
self.loadout = loadout
self.use_custom_loadout = False
self.properties: dict[str, bool | float | int] = {}
@property
def is_player(self) -> bool:
if self.pilot is None:
return False
return self.pilot.player

89
game/ato/flightmembers.py Normal file
View File

@@ -0,0 +1,89 @@
from __future__ import annotations
from collections.abc import Iterator
from typing import Optional, TYPE_CHECKING
from .flightmember import FlightMember
from .flightroster import FlightRoster
from .iflightroster import IFlightRoster
from .loadouts import Loadout
from ..data.weapons import Weapon
if TYPE_CHECKING:
from game.squadrons import Pilot
from .flight import Flight
class FlightMembers(IFlightRoster):
def __init__(self, flight: Flight, initial_size: int = 0) -> None:
self.flight = flight
self.members: list[FlightMember] = []
self.resize(initial_size)
@staticmethod
def from_roster(flight: Flight, roster: FlightRoster) -> FlightMembers:
members = FlightMembers(flight)
loadout = Loadout.default_for(flight)
if flight.squadron.aircraft.name == "F-15I Ra'am":
loadout.pylons[16] = Weapon.with_clsid(
"{IDF_MODS_PROJECT_F-15I_Raam_Dome}"
)
members.members = [FlightMember(p, loadout) for p in roster.pilots]
return members
def iter_pilots(self) -> Iterator[Pilot | None]:
yield from (m.pilot for m in self.members)
def pilot_at(self, idx: int) -> Pilot | None:
return self.members[idx].pilot
@property
def max_size(self) -> int:
return len(self.members)
@property
def player_count(self) -> int:
return len([m for m in self.members if m.is_player])
@property
def missing_pilots(self) -> int:
return len([m for m in self.members if m.pilot is None])
def resize(self, new_size: int) -> None:
if self.max_size > new_size:
self.flight.squadron.return_pilots(
[m.pilot for m in self.members[new_size:] if m.pilot is not None]
)
self.members = self.members[:new_size]
return
if self.max_size:
loadout = self.members[0].loadout.clone()
else:
loadout = Loadout.default_for(self.flight)
if self.flight.squadron.aircraft.name == "F-15I Ra'am":
loadout.pylons[16] = Weapon.with_clsid(
"{IDF_MODS_PROJECT_F-15I_Raam_Dome}"
)
for _ in range(new_size - self.max_size):
member = FlightMember(self.flight.squadron.claim_available_pilot(), loadout)
member.use_custom_loadout = loadout.is_custom
self.members.append(member)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
if pilot is not None:
self.flight.squadron.claim_pilot(pilot)
if (current_pilot := self.pilot_at(index)) is not None:
self.flight.squadron.return_pilot(current_pilot)
self.members[index].pilot = pilot
def clear(self) -> None:
self.flight.squadron.return_pilots(
[p for p in self.iter_pilots() if p is not None]
)
def use_same_loadout_for_all_members(self) -> None:
if not self.members:
return
loadout = self.members[0].loadout
for member in self.members[1:]:
member.loadout = loadout.clone()

View File

@@ -1,29 +1,30 @@
from __future__ import annotations
from collections.abc import Iterator
from typing import Optional, TYPE_CHECKING
from game.ato.iflightroster import IFlightRoster
if TYPE_CHECKING:
from game.squadrons import Squadron, Pilot
class FlightRoster:
class FlightRoster(IFlightRoster):
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
self.squadron = squadron
self.pilots: list[Optional[Pilot]] = []
self.resize(initial_size)
def iter_pilots(self) -> Iterator[Pilot | None]:
yield from self.pilots
def pilot_at(self, idx: int) -> Pilot | None:
return self.pilots[idx]
@property
def max_size(self) -> int:
return len(self.pilots)
@property
def player_count(self) -> int:
return len([p for p in self.pilots if p is not None and p.player])
@property
def missing_pilots(self) -> int:
return len([p for p in self.pilots if p is None])
def resize(self, new_size: int) -> None:
if self.max_size > new_size:
self.squadron.return_pilots(

34
game/ato/iflightroster.py Normal file
View File

@@ -0,0 +1,34 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional, TYPE_CHECKING, Iterator
if TYPE_CHECKING:
from game.squadrons import Pilot
class IFlightRoster(ABC):
@abstractmethod
def iter_pilots(self) -> Iterator[Pilot | None]:
...
@abstractmethod
def pilot_at(self, idx: int) -> Pilot | None:
...
@property
@abstractmethod
def max_size(self) -> int:
...
@abstractmethod
def resize(self, new_size: int) -> None:
...
@abstractmethod
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
...
@abstractmethod
def clear(self) -> None:
...

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import copy
import datetime
import logging
from collections.abc import Iterable
@@ -35,6 +36,11 @@ class Loadout:
def derive_custom(self, name: str) -> Loadout:
return Loadout(name, self.pylons, self.date, is_custom=True)
def clone(self) -> Loadout:
return Loadout(
self.name, dict(self.pylons), copy.deepcopy(self.date), self.is_custom
)
def has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
for weapon in self.pylons.values():
if weapon is not None and weapon.weapon_group.type is weapon_type:

View File

@@ -10,7 +10,7 @@ from pathlib import Path
from typing import Iterator, Optional, Any, ClassVar
import yaml
from dcs.unitgroup import FlyingGroup
from dcs.flyingunit import FlyingUnit
from dcs.weapons_data import weapon_ids
from game.dcs.aircrafttype import AircraftType
@@ -278,10 +278,10 @@ class Pylon:
# configuration.
return weapon in self.allowed or weapon.clsid == "<CLEAN>"
def equip(self, group: FlyingGroup[Any], weapon: Weapon) -> None:
def equip(self, unit: FlyingUnit, weapon: Weapon) -> None:
if not self.can_equip(weapon):
logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
unit.load_pylon(self.make_pydcs_assignment(weapon), self.number)
def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
return self.number, weapon.pydcs_data

View File

@@ -27,6 +27,7 @@ from .aircraftbehavior import AircraftBehavior
from .aircraftpainter import AircraftPainter
from .flightdata import FlightData
from .waypoints import WaypointGenerator
from ...ato.flightmember import FlightMember
from ...ato.flightplans.aewc import AewcFlightPlan
from ...ato.flightplans.packagerefueling import PackageRefuelingFlightPlan
from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
@@ -67,13 +68,13 @@ class FlightGroupConfigurator:
AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group)
AircraftPainter(self.flight, self.group).apply_livery()
self.setup_props()
self.setup_payload()
self.setup_payloads()
self.setup_fuel()
flight_channel = self.setup_radios()
laser_codes: list[Optional[int]] = []
for unit, pilot in zip(self.group.units, self.flight.roster.pilots):
self.configure_flight_member(unit, pilot, laser_codes)
for unit, member in zip(self.group.units, self.flight.iter_members()):
self.configure_flight_member(unit, member, laser_codes)
divert = None
if self.flight.divert is not None:
@@ -143,21 +144,20 @@ class FlightGroupConfigurator:
)
def configure_flight_member(
self, unit: FlyingUnit, pilot: Optional[Pilot], laser_codes: list[Optional[int]]
self, unit: FlyingUnit, member: FlightMember, laser_codes: list[Optional[int]]
) -> None:
player = pilot is not None and pilot.player
self.set_skill(unit, pilot)
if self.flight.loadout.has_weapon_of_type(WeaponTypeEnum.TGP) and player:
self.set_skill(unit, member)
if member.loadout.has_weapon_of_type(WeaponTypeEnum.TGP) and member.is_player:
laser_codes.append(self.laser_code_registry.get_next_laser_code())
else:
laser_codes.append(None)
settings = self.flight.coalition.game.settings
if not player or not settings.plugins.get("ewrj"):
if not member.is_player or not settings.plugins.get("ewrj"):
return
jammer_required = settings.plugin_option("ewrj.ecm_required")
if jammer_required:
ecm = WeaponTypeEnum.JAMMER
if not self.flight.loadout.has_weapon_of_type(ecm):
if not member.loadout.has_weapon_of_type(ecm):
return
ewrj_menu_trigger = TriggerStart(comment=f"EWRJ-{unit.name}")
ewrj_menu_trigger.add_action(DoScript(String(f'EWJamming("{unit.name}")')))
@@ -221,9 +221,9 @@ class FlightGroupConfigurator:
)
)
def set_skill(self, unit: FlyingUnit, pilot: Optional[Pilot]) -> None:
if pilot is None or not pilot.player:
unit.skill = self.skill_level_for(unit, pilot)
def set_skill(self, unit: FlyingUnit, member: FlightMember) -> None:
if not member.is_player:
unit.skill = self.skill_level_for(unit, member.pilot)
return
if self.use_client or "Pilot #1" not in unit.name:
@@ -260,15 +260,18 @@ class FlightGroupConfigurator:
return levels[new_level]
def setup_props(self) -> None:
for prop_id, value in self.flight.props.items():
for unit in self.group.units:
for unit, member in zip(self.group.units, self.flight.iter_members()):
for prop_id, value in member.properties.items():
unit.set_property(prop_id, value)
def setup_payload(self) -> None:
for p in self.group.units:
p.pylons.clear()
def setup_payloads(self) -> None:
for unit, member in zip(self.group.units, self.flight.iter_members()):
self.setup_payload(unit, member)
loadout = self.flight.loadout
def setup_payload(self, unit: FlyingUnit, member: FlightMember) -> None:
unit.pylons.clear()
loadout = member.loadout
if self.game.settings.restrict_weapons_by_date:
loadout = loadout.degrade_for_date(self.flight.unit_type, self.game.date)
@@ -276,7 +279,7 @@ class FlightGroupConfigurator:
if weapon is None:
continue
pylon = Pylon.for_aircraft(self.flight.unit_type, pylon_number)
pylon.equip(self.group, weapon)
pylon.equip(unit, weapon)
def setup_fuel(self) -> None:
fuel = self.flight.state.estimate_fuel()
@@ -287,5 +290,8 @@ class FlightGroupConfigurator:
"starting fuel to 100kg."
)
fuel = 100
for unit, pilot in zip(self.group.units, self.flight.roster.pilots):
unit.fuel = fuel
for unit, pilot in zip(self.group.units, self.flight.roster.iter_pilots()):
if pilot is not None and pilot.player:
unit.fuel = fuel
else:
unit.fuel = self.flight.fuel

View File

@@ -29,7 +29,7 @@ class OcaRunwayIngressBuilder(PydcsWaypointBuilder):
# for more details.
# The LGB work around assumes the Airfield position in DCS is on a runway, which seems
# to be the case for most if not all airfields.
if self.flight.loadout.has_weapon_of_type(WeaponType.LGB):
if self.flight.any_member_has_weapon_of_type(WeaponType.LGB):
waypoint.tasks.append(
Bombing(
position=target.position,

View File

@@ -53,7 +53,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
)
waypoint.tasks.append(attack_task)
if self.flight.loadout.has_weapon_of_type(WeaponType.ARM):
if self.flight.any_member_has_weapon_of_type(WeaponType.ARM):
# Special handling for ARM Weapon types:
# The SEAD flight will Search for the targeted group and then engage it
# if it is found only. This will prevent AI from having huge problems

View File

@@ -60,7 +60,7 @@ class MissionResultsProcessor:
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
for package in ato.packages:
for flight in package.flights:
for idx, pilot in enumerate(flight.roster.pilots):
for idx, pilot in enumerate(flight.roster.iter_pilots()):
if pilot is None:
logging.error(
f"Cannot award experience to pilot #{idx} of {flight} "

View File

@@ -68,7 +68,7 @@ class UnitMap:
self.airlifts: Dict[str, AirliftUnits] = {}
def add_aircraft(self, group: FlyingGroup[Any], flight: Flight) -> None:
for pilot, unit in zip(flight.roster.pilots, group.units):
for pilot, unit in zip(flight.roster.iter_pilots(), group.units):
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)