mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Allow per pilot loadouts and properties.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3092.
This commit is contained in:
@@ -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
22
game/ato/flightmember.py
Normal 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
89
game/ato/flightmembers.py
Normal 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()
|
||||
@@ -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
34
game/ato/iflightroster.py
Normal 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:
|
||||
...
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} "
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user