mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Refactor transfers to support unfulfilled orders.
This gives a clean break between the transfer request and the type of transport allocated to make way for transports that need to switch types (to support driving to a port, then getting on a ship, to a train, then back on the road, etc). https://github.com/Khopa/dcs_liberation/issues/823
This commit is contained in:
parent
d6c84e362f
commit
d80f7ebf3b
@ -20,11 +20,14 @@ if TYPE_CHECKING:
|
||||
from game.inventory import ControlPointAircraftInventory
|
||||
|
||||
|
||||
# TODO: Remove base classes.
|
||||
# Eventually we'll want multi-mode transfers (convoy from factory to port, onto a ship,
|
||||
# then airlifted to the final destination, etc). To do this we'll need to make the
|
||||
# transfer *order* represent the full journey and let classes like Convoy handle the
|
||||
# individual hops.
|
||||
class Transport:
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
raise NotImplementedError
|
||||
|
||||
def description(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransferOrder:
|
||||
"""The base type of all transfer orders.
|
||||
@ -38,59 +41,30 @@ class TransferOrder:
|
||||
#: The location the units are transferring to.
|
||||
destination: ControlPoint
|
||||
|
||||
#: The current position of the group being transferred. Groups may make multiple
|
||||
#: stops and can switch transport modes before reaching their destination.
|
||||
position: ControlPoint = field(init=False)
|
||||
|
||||
#: True if the transfer order belongs to the player.
|
||||
player: bool
|
||||
player: bool = field(init=False)
|
||||
|
||||
#: The units being transferred.
|
||||
units: Dict[Type[VehicleType], int]
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoadTransferOrder(TransferOrder):
|
||||
"""A transfer order that moves units by road."""
|
||||
|
||||
#: The current position of the group being transferred. Groups move one control
|
||||
#: point a turn through the supply line.
|
||||
position: ControlPoint = field(init=False)
|
||||
transport: Optional[Transport] = field(default=None)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.position = self.origin
|
||||
|
||||
def path(self) -> List[ControlPoint]:
|
||||
supply_route = SupplyRoute.for_control_point(self.position)
|
||||
return supply_route.shortest_path_between(self.position, self.destination)
|
||||
|
||||
def next_stop(self) -> ControlPoint:
|
||||
return self.path()[0]
|
||||
self.player = self.origin.is_friendly(to_player=True)
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
path = self.path()
|
||||
if len(path) == 1:
|
||||
turns = "1 turn"
|
||||
else:
|
||||
turns = f"{len(path)} turns"
|
||||
return f"Currently at {self.position}. Arrives at destination in {turns}."
|
||||
if self.transport is None:
|
||||
return "No transports available"
|
||||
return self.transport.description()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirliftOrder(TransferOrder):
|
||||
"""A transfer order that moves units by cargo planes and helicopters."""
|
||||
|
||||
flight: Flight
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "Airlift"
|
||||
|
||||
def iter_units(self) -> Iterator[Type[VehicleType]]:
|
||||
for unit_type, count in self.units.items():
|
||||
for _ in range(count):
|
||||
yield unit_type
|
||||
def kill_all(self) -> None:
|
||||
self.units.clear()
|
||||
|
||||
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
|
||||
if unit_type in self.units:
|
||||
@ -98,54 +72,102 @@ class AirliftOrder(TransferOrder):
|
||||
return
|
||||
raise KeyError
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum(c for c in self.units.values())
|
||||
|
||||
def iter_units(self) -> Iterator[Type[VehicleType]]:
|
||||
for unit_type, count in self.units.items():
|
||||
for _ in range(count):
|
||||
yield unit_type
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self.destination == self.position or not self.units
|
||||
|
||||
def disband_at(self, location: ControlPoint) -> None:
|
||||
logging.info(f"Units halting at {location}.")
|
||||
location.base.commision_units(self.units)
|
||||
self.units.clear()
|
||||
|
||||
def proceed(self) -> None:
|
||||
if self.transport is None:
|
||||
return
|
||||
|
||||
if not self.destination.is_friendly(self.player):
|
||||
logging.info(f"Transfer destination {self.destination} was captured.")
|
||||
if self.position.is_friendly(self.player):
|
||||
self.disband_at(self.position)
|
||||
elif (escape_route := self.transport.find_escape_route()) is not None:
|
||||
self.disband_at(escape_route)
|
||||
else:
|
||||
logging.info(
|
||||
f"No escape route available. Units were surrounded and destroyed "
|
||||
"during transfer."
|
||||
)
|
||||
self.kill_all()
|
||||
return
|
||||
|
||||
self.position = self.destination
|
||||
self.transport = None
|
||||
|
||||
if self.completed:
|
||||
self.disband_at(self.position)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Airlift(Transport):
|
||||
"""A transfer order that moves units by cargo planes and helicopters."""
|
||||
|
||||
transfer: TransferOrder
|
||||
|
||||
flight: Flight
|
||||
|
||||
@property
|
||||
def units(self) -> Dict[Type[VehicleType], int]:
|
||||
return self.transfer.units
|
||||
|
||||
@property
|
||||
def player_owned(self) -> bool:
|
||||
return self.transfer.player
|
||||
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
# TODO: Move units to closest base.
|
||||
return None
|
||||
|
||||
def description(self) -> str:
|
||||
return f"Being airlifted by {self.flight}"
|
||||
|
||||
|
||||
class AirliftPlanner:
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
pickup: ControlPoint,
|
||||
drop_off: ControlPoint,
|
||||
units: Dict[Type[VehicleType], int],
|
||||
) -> None:
|
||||
def __init__(self, game: Game, transfer: TransferOrder) -> None:
|
||||
self.game = game
|
||||
self.pickup = pickup
|
||||
self.drop_off = drop_off
|
||||
self.units = units
|
||||
self.for_player = drop_off.captured
|
||||
self.package = Package(target=drop_off, auto_asap=True)
|
||||
self.transfer = transfer
|
||||
self.for_player = transfer.destination.captured
|
||||
self.package = Package(target=transfer.destination, auto_asap=True)
|
||||
|
||||
def create_package_for_airlift(self) -> Dict[Type[VehicleType], int]:
|
||||
def create_package_for_airlift(self) -> None:
|
||||
for cp in self.game.theater.player_points():
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
for unit_type, available in inventory.all_aircraft:
|
||||
if unit_type.helicopter:
|
||||
while available and self.needed_capacity:
|
||||
while available and self.transfer.transport is None:
|
||||
flight_size = self.create_airlift_flight(unit_type, inventory)
|
||||
available -= flight_size
|
||||
self.game.ato_for(self.for_player).add_package(self.package)
|
||||
return self.units
|
||||
|
||||
def take_units(self, count: int) -> Dict[Type[VehicleType], int]:
|
||||
taken = {}
|
||||
for unit_type, remaining in self.units.items():
|
||||
take = min(remaining, count)
|
||||
count -= take
|
||||
self.units[unit_type] -= take
|
||||
taken[unit_type] = take
|
||||
if not count:
|
||||
break
|
||||
return taken
|
||||
|
||||
@property
|
||||
def needed_capacity(self) -> int:
|
||||
return sum(c for c in self.units.values())
|
||||
|
||||
def create_airlift_flight(
|
||||
self, unit_type: Type[FlyingType], inventory: ControlPointAircraftInventory
|
||||
) -> int:
|
||||
available = inventory.available(unit_type)
|
||||
# 4 is the max flight size in DCS.
|
||||
flight_size = min(self.needed_capacity, available, 4)
|
||||
flight_size = min(self.transfer.size, available, 4)
|
||||
|
||||
if flight_size < self.transfer.size:
|
||||
transfer = self.game.transfers.split_transfer(self.transfer, flight_size)
|
||||
else:
|
||||
transfer = self.transfer
|
||||
|
||||
flight = Flight(
|
||||
self.package,
|
||||
self.game.player_country,
|
||||
@ -156,31 +178,25 @@ class AirliftPlanner:
|
||||
departure=inventory.control_point,
|
||||
arrival=inventory.control_point,
|
||||
divert=None,
|
||||
cargo=transfer,
|
||||
)
|
||||
|
||||
transfer = AirliftOrder(
|
||||
player=True,
|
||||
origin=self.pickup,
|
||||
destination=self.drop_off,
|
||||
units=self.take_units(flight_size),
|
||||
flight=flight,
|
||||
)
|
||||
flight.cargo = transfer
|
||||
transport = Airlift(transfer, flight)
|
||||
transfer.transport = transport
|
||||
|
||||
self.package.add_flight(flight)
|
||||
planner = FlightPlanBuilder(self.game, self.package, self.for_player)
|
||||
planner.populate_flight_plan(flight)
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
self.game.transfers.new_transfer(transfer)
|
||||
return flight_size
|
||||
|
||||
|
||||
class Convoy(MissionTarget):
|
||||
class Convoy(MissionTarget, Transport):
|
||||
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
||||
super().__init__(namegen.next_convoy_name(), origin.position)
|
||||
self.origin = origin
|
||||
self.destination = destination
|
||||
self.transfers: List[RoadTransferOrder] = []
|
||||
self.transfers: List[TransferOrder] = []
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
if self.is_friendly(for_player):
|
||||
@ -192,17 +208,20 @@ class Convoy(MissionTarget):
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
return self.origin.captured
|
||||
|
||||
def add_units(self, transfer: RoadTransferOrder) -> None:
|
||||
def add_units(self, transfer: TransferOrder) -> None:
|
||||
self.transfers.append(transfer)
|
||||
transfer.transport = self
|
||||
|
||||
def remove_units(self, transfer: RoadTransferOrder) -> None:
|
||||
def remove_units(self, transfer: TransferOrder) -> None:
|
||||
self.transfers.remove(transfer)
|
||||
|
||||
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
|
||||
for transfer in self.transfers:
|
||||
if unit_type in transfer.units:
|
||||
transfer.units[unit_type] -= 1
|
||||
try:
|
||||
transfer.kill_unit(unit_type)
|
||||
return
|
||||
except KeyError:
|
||||
pass
|
||||
raise KeyError
|
||||
|
||||
@property
|
||||
@ -221,6 +240,12 @@ class Convoy(MissionTarget):
|
||||
def player_owned(self) -> bool:
|
||||
return self.origin.captured
|
||||
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
return None
|
||||
|
||||
def description(self) -> str:
|
||||
return f"In a convoy to {self.destination}"
|
||||
|
||||
|
||||
class ConvoyMap:
|
||||
def __init__(self) -> None:
|
||||
@ -255,18 +280,21 @@ class ConvoyMap:
|
||||
def disband_convoy(self, convoy: Convoy) -> None:
|
||||
del self.convoys[convoy.origin][convoy.destination]
|
||||
|
||||
def add(self, transfer: RoadTransferOrder) -> None:
|
||||
next_stop = transfer.next_stop()
|
||||
@staticmethod
|
||||
def path_for(transfer: TransferOrder) -> List[ControlPoint]:
|
||||
supply_route = SupplyRoute.for_control_point(transfer.position)
|
||||
return supply_route.shortest_path_between(
|
||||
transfer.position, transfer.destination
|
||||
)
|
||||
|
||||
def next_stop_for(self, transfer: TransferOrder) -> ControlPoint:
|
||||
return self.path_for(transfer)[0]
|
||||
|
||||
def add(self, transfer: TransferOrder) -> None:
|
||||
next_stop = self.next_stop_for(transfer)
|
||||
self.find_or_create_convoy(transfer.position, next_stop).add_units(transfer)
|
||||
|
||||
def remove(self, transfer: RoadTransferOrder) -> None:
|
||||
next_stop = transfer.next_stop()
|
||||
convoy = self.find_convoy(transfer.position, next_stop)
|
||||
if convoy is None:
|
||||
logging.error(
|
||||
f"Attempting to remove {transfer} from convoy but it is in no convoy."
|
||||
)
|
||||
return
|
||||
def remove(self, convoy: Convoy, transfer: TransferOrder) -> None:
|
||||
convoy.remove_units(transfer)
|
||||
if not convoy.transfers:
|
||||
self.disband_convoy(convoy)
|
||||
@ -298,43 +326,63 @@ class PendingTransfers:
|
||||
def index_of_transfer(self, transfer: TransferOrder) -> int:
|
||||
return self.pending_transfers.index(transfer)
|
||||
|
||||
# TODO: Move airlift arrangements here?
|
||||
@singledispatchmethod
|
||||
def arrange_transport(self, transfer) -> None:
|
||||
pass
|
||||
|
||||
@arrange_transport.register
|
||||
def _arrange_transport_road(self, transfer: RoadTransferOrder) -> None:
|
||||
self.convoys.add(transfer)
|
||||
def arrange_transport(self, transfer: TransferOrder) -> None:
|
||||
supply_route = SupplyRoute.for_control_point(transfer.position)
|
||||
if transfer.destination in supply_route:
|
||||
self.convoys.add(transfer)
|
||||
else:
|
||||
AirliftPlanner(self.game, transfer).create_package_for_airlift()
|
||||
|
||||
def new_transfer(self, transfer: TransferOrder) -> None:
|
||||
transfer.origin.base.commit_losses(transfer.units)
|
||||
self.pending_transfers.append(transfer)
|
||||
self.arrange_transport(transfer)
|
||||
|
||||
def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder:
|
||||
"""Creates a smaller transfer that is a subset of the original."""
|
||||
if transfer.size <= size:
|
||||
raise ValueError
|
||||
|
||||
units = {}
|
||||
for unit_type, remaining in transfer.units.items():
|
||||
take = min(remaining, size)
|
||||
size -= take
|
||||
transfer.units[unit_type] -= take
|
||||
units[unit_type] = take
|
||||
if not size:
|
||||
break
|
||||
new_transfer = TransferOrder(transfer.origin, transfer.destination, units)
|
||||
self.pending_transfers.append(new_transfer)
|
||||
return new_transfer
|
||||
|
||||
@singledispatchmethod
|
||||
def cancel_transport(self, transfer) -> None:
|
||||
def cancel_transport(self, transfer: TransferOrder, transport) -> None:
|
||||
pass
|
||||
|
||||
@cancel_transport.register
|
||||
def _cancel_transport_air(self, transfer: AirliftOrder) -> None:
|
||||
flight = transfer.flight
|
||||
def _cancel_transport_air(
|
||||
self, _transfer: TransferOrder, transport: Airlift
|
||||
) -> None:
|
||||
flight = transport.flight
|
||||
flight.package.remove_flight(flight)
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
|
||||
@cancel_transport.register
|
||||
def _cancel_transport_road(self, transfer: RoadTransferOrder) -> None:
|
||||
self.convoys.remove(transfer)
|
||||
def _cancel_transport_convoy(
|
||||
self, transfer: TransferOrder, transport: Convoy
|
||||
) -> None:
|
||||
self.convoys.remove(transport, transfer)
|
||||
|
||||
def cancel_transfer(self, transfer: TransferOrder) -> None:
|
||||
self.cancel_transport(transfer)
|
||||
if transfer.transport is not None:
|
||||
self.cancel_transport(transfer, transfer.transport)
|
||||
self.pending_transfers.remove(transfer)
|
||||
transfer.origin.base.commision_units(transfer.units)
|
||||
|
||||
def perform_transfers(self) -> None:
|
||||
incomplete = []
|
||||
for transfer in self.pending_transfers:
|
||||
if not self.perform_transfer(transfer):
|
||||
transfer.proceed()
|
||||
if not transfer.completed:
|
||||
incomplete.append(transfer)
|
||||
self.pending_transfers = incomplete
|
||||
self.rebuild_convoys()
|
||||
@ -343,80 +391,3 @@ class PendingTransfers:
|
||||
self.convoys.disband_all()
|
||||
for transfer in self.pending_transfers:
|
||||
self.arrange_transport(transfer)
|
||||
|
||||
@singledispatchmethod
|
||||
def perform_transfer(self, transfer) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@perform_transfer.register
|
||||
def _perform_transfer_air(self, transfer: AirliftOrder) -> bool:
|
||||
if transfer.player != transfer.destination.captured:
|
||||
logging.info(
|
||||
f"Transfer destination {transfer.destination} was captured. Cancelling "
|
||||
"transport."
|
||||
)
|
||||
transfer.origin.base.commision_units(transfer.units)
|
||||
return True
|
||||
|
||||
transfer.destination.base.commision_units(transfer.units)
|
||||
return True
|
||||
|
||||
@perform_transfer.register
|
||||
def _perform_transfer_road(self, transfer: RoadTransferOrder) -> bool:
|
||||
# TODO: Can be improved to use the convoy map.
|
||||
# The convoy map already has a lot of the data that we're recomputing here.
|
||||
if transfer.player != transfer.destination.captured:
|
||||
logging.info(
|
||||
f"Transfer destination {transfer.destination.name} was captured."
|
||||
)
|
||||
self.handle_route_interrupted(transfer)
|
||||
return True
|
||||
|
||||
supply_route = SupplyRoute.for_control_point(transfer.destination)
|
||||
if transfer.position not in supply_route:
|
||||
logging.info(
|
||||
f"Route from {transfer.position.name} to {transfer.destination.name} "
|
||||
"was cut off."
|
||||
)
|
||||
self.handle_route_interrupted(transfer)
|
||||
return True
|
||||
|
||||
path = transfer.path()
|
||||
next_hop = path[0]
|
||||
if next_hop == transfer.destination:
|
||||
logging.info(
|
||||
f"Units transferred from {transfer.origin.name} to "
|
||||
f"{transfer.destination.name}"
|
||||
)
|
||||
transfer.destination.base.commision_units(transfer.units)
|
||||
return True
|
||||
|
||||
logging.info(
|
||||
f"Units transferring from {transfer.origin.name} to "
|
||||
f"{transfer.destination.name} arrived at {next_hop.name}. {len(path) - 1} "
|
||||
"turns remaining."
|
||||
)
|
||||
transfer.position = next_hop
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def handle_route_interrupted(transfer: RoadTransferOrder):
|
||||
# Halt the transfer in place if safe.
|
||||
if transfer.player == transfer.position.captured:
|
||||
logging.info(f"Transferring units are halting at {transfer.position.name}.")
|
||||
transfer.position.base.commision_units(transfer.units)
|
||||
return
|
||||
|
||||
# If the current position was captured attempt to divert to a neighboring
|
||||
# friendly CP.
|
||||
for connected in transfer.position.connected_points:
|
||||
if connected.captured == transfer.player:
|
||||
logging.info(f"Transferring units are re-routing to {connected.name}.")
|
||||
connected.base.commision_units(transfer.units)
|
||||
return
|
||||
|
||||
# If the units are cutoff they are destroyed.
|
||||
logging.info(
|
||||
f"Both {transfer.position.name} and {transfer.destination.name} were "
|
||||
"captured. Units were surrounded and destroyed during transfer."
|
||||
)
|
||||
|
||||
@ -8,11 +8,9 @@ from typing import Dict, Optional, TYPE_CHECKING, Type
|
||||
from dcs.unittype import UnitType, VehicleType
|
||||
|
||||
from game.theater import ControlPoint, SupplyRoute
|
||||
from gen.ato import Package
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import Flight
|
||||
from .db import PRICES
|
||||
from .transfers import AirliftOrder, AirliftPlanner, RoadTransferOrder
|
||||
from .transfers import TransferOrder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .game import Game
|
||||
@ -111,44 +109,14 @@ class PendingUnitDeliveries:
|
||||
ground_unit_source.control_point.base.commision_units(
|
||||
units_needing_transfer
|
||||
)
|
||||
if ground_unit_source.requires_airlift:
|
||||
self.create_air_transfer(
|
||||
game, ground_unit_source.control_point, units_needing_transfer
|
||||
)
|
||||
else:
|
||||
self.create_road_transfer(
|
||||
game, ground_unit_source.control_point, units_needing_transfer
|
||||
)
|
||||
self.create_transfer(
|
||||
game, ground_unit_source.control_point, units_needing_transfer
|
||||
)
|
||||
|
||||
def create_air_transfer(
|
||||
def create_transfer(
|
||||
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
|
||||
) -> None:
|
||||
planner = AirliftPlanner(game, source, self.destination, units)
|
||||
leftovers = planner.create_package_for_airlift()
|
||||
if leftovers:
|
||||
game.message(
|
||||
f"No airlift capacity remaining for {self.destination}. "
|
||||
"Remaining unit orders were refunded."
|
||||
)
|
||||
self.refund(game, leftovers)
|
||||
source.base.commit_losses(leftovers)
|
||||
|
||||
def find_transport_for(
|
||||
self,
|
||||
origin: ControlPoint,
|
||||
destination: ControlPoint,
|
||||
units: Dict[Type[VehicleType], int],
|
||||
) -> Optional[Flight]:
|
||||
pass
|
||||
|
||||
def create_road_transfer(
|
||||
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
|
||||
) -> None:
|
||||
game.transfers.new_transfer(
|
||||
RoadTransferOrder(
|
||||
source, self.destination, self.destination.captured, units
|
||||
)
|
||||
)
|
||||
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
|
||||
|
||||
def find_ground_unit_source(self, game: Game) -> Optional[GroundUnitSource]:
|
||||
# This is running *after* the turn counter has been incremented, so this is the
|
||||
|
||||
@ -9,7 +9,7 @@ from dcs.unittype import VehicleType
|
||||
from game import db
|
||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import BuildingGroundObject
|
||||
from game.transfers import AirliftOrder, Convoy
|
||||
from game.transfers import Convoy, TransferOrder
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ class ConvoyUnit:
|
||||
@dataclass(frozen=True)
|
||||
class AirliftUnit:
|
||||
unit_type: Type[VehicleType]
|
||||
transfer: AirliftOrder
|
||||
transfer: TransferOrder
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -149,17 +149,14 @@ class UnitMap:
|
||||
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
|
||||
return self.convoys.get(name, None)
|
||||
|
||||
def add_airlift_units(self, group: FlyingGroup, airlift: AirliftOrder) -> None:
|
||||
for transport, cargo_type in zip(group.units, airlift.iter_units()):
|
||||
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
|
||||
for transport, cargo_type in zip(group.units, transfer.iter_units()):
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(transport.name)
|
||||
if name in self.airlifts:
|
||||
raise RuntimeError(f"Duplicate airlift unit: {name}")
|
||||
unit_type = db.unit_type_from_name(transport.type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(f"Unknown unit type: {transport.type}")
|
||||
self.airlifts[name] = AirliftUnit(cargo_type, airlift)
|
||||
self.airlifts[name] = AirliftUnit(cargo_type, transfer)
|
||||
|
||||
def airlift_unit(self, name: str) -> Optional[AirliftUnit]:
|
||||
return self.airlifts.get(name, None)
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union
|
||||
@ -42,6 +42,7 @@ from dcs.planes import (
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.task import (
|
||||
AWACS,
|
||||
AWACSTaskAction,
|
||||
AntishipStrike,
|
||||
AttackGroup,
|
||||
Bombing,
|
||||
@ -66,13 +67,12 @@ from dcs.task import (
|
||||
StartCommand,
|
||||
Targets,
|
||||
Task,
|
||||
Transport,
|
||||
WeaponType,
|
||||
AWACSTaskAction,
|
||||
SetFrequencyCommand,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
@ -88,7 +88,7 @@ from game.theater.controlpoint import (
|
||||
OffMapSpawn,
|
||||
)
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from game.transfers import Convoy, RoadTransferOrder
|
||||
from game.transfers import Convoy
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
@ -101,17 +101,17 @@ from gen.flights.flight import (
|
||||
)
|
||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||
from gen.runways import RunwayData
|
||||
from .airsupportgen import AirSupport, AwacsInfo
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .flights.flightplan import (
|
||||
AwacsFlightPlan,
|
||||
CasFlightPlan,
|
||||
LoiterFlightPlan,
|
||||
PatrollingFlightPlan,
|
||||
SweepFlightPlan,
|
||||
AwacsFlightPlan,
|
||||
)
|
||||
from .flights.traveltime import GroundSpeed, TotEstimator
|
||||
from .naming import namegen
|
||||
from .airsupportgen import AirSupport, AwacsInfo
|
||||
from .callsigns import callsign_for_support_unit
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -1421,6 +1421,25 @@ class AircraftConflictGenerator:
|
||||
group, roe=OptROE.Values.OpenFire, restrict_jettison=True
|
||||
)
|
||||
|
||||
def configure_transport(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
) -> None:
|
||||
# Escort groups are actually given the CAP task so they can perform the
|
||||
# Search Then Engage task, which we have to use instead of the Escort
|
||||
# task for the reasons explained in JoinPointBuilder.
|
||||
group.task = Transport.name
|
||||
self._setup_group(group, Transport, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.WeaponHold,
|
||||
restrict_jettison=True,
|
||||
)
|
||||
|
||||
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
|
||||
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
||||
self.configure_behavior(group)
|
||||
@ -1459,6 +1478,8 @@ class AircraftConflictGenerator:
|
||||
self.configure_runway_attack(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.OCA_AIRCRAFT:
|
||||
self.configure_oca_strike(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.TRANSPORT:
|
||||
self.configure_transport(group, package, flight, dynamic_runways)
|
||||
else:
|
||||
self.configure_unknown_task(group, flight)
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ from dcs.unit import Vehicle
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.transfers import Convoy, RoadTransferOrder
|
||||
from game.transfers import Convoy
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import kph
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||
from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.transfers import AirliftOrder
|
||||
from game.transfers import Airlift, TransferOrder
|
||||
from gen.ato import Package
|
||||
from gen.flights.flightplan import FlightPlan
|
||||
|
||||
@ -167,7 +167,7 @@ class Flight:
|
||||
arrival: ControlPoint,
|
||||
divert: Optional[ControlPoint],
|
||||
custom_name: Optional[str] = None,
|
||||
cargo: Optional[AirliftOrder] = None,
|
||||
cargo: Optional[TransferOrder] = None,
|
||||
) -> None:
|
||||
self.package = package
|
||||
self.country = country
|
||||
|
||||
@ -163,12 +163,10 @@ class PackageModel(QAbstractListModel):
|
||||
"""Removes the given flight from the package."""
|
||||
index = self.package.flights.index(flight)
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
if flight.cargo is None:
|
||||
self.game_model.game.aircraft_inventory.return_from_flight(flight)
|
||||
self.package.remove_flight(flight)
|
||||
else:
|
||||
# Deleted transfers will clean up after themselves.
|
||||
self.game_model.transfer_model.cancel_transfer(flight.cargo)
|
||||
if flight.cargo is not None:
|
||||
flight.cargo.transport = None
|
||||
self.game_model.game.aircraft_inventory.return_from_flight(flight)
|
||||
self.package.remove_flight(flight)
|
||||
self.endRemoveRows()
|
||||
self.update_tot()
|
||||
|
||||
@ -261,7 +259,7 @@ class AtoModel(QAbstractListModel):
|
||||
for flight in package.flights:
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
if flight.cargo is not None:
|
||||
self.game_model.transfer_model.cancel_transfer(flight.cargo)
|
||||
flight.cargo.transport = None
|
||||
self.endRemoveRows()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.client_slots_changed.emit()
|
||||
|
||||
@ -39,12 +39,12 @@ from shapely.geometry import (
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game
|
||||
from game.navmesh import NavMesh
|
||||
from game.theater import ControlPoint, Enum, SupplyRoute
|
||||
from game.theater import ControlPoint, Enum
|
||||
from game.theater.conflicttheater import FrontLine, ReferencePoint
|
||||
from game.theater.theatergroundobject import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.transfers import Convoy, RoadTransferOrder
|
||||
from game.transfers import Convoy
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from game.weather import TimeOfDay
|
||||
from gen import Conflict, Package
|
||||
@ -55,12 +55,10 @@ from gen.flights.flight import (
|
||||
FlightWaypointType,
|
||||
)
|
||||
from gen.flights.flightplan import (
|
||||
BarCapFlightPlan,
|
||||
FlightPlan,
|
||||
FlightPlanBuilder,
|
||||
InvalidObjectiveLocation,
|
||||
PatrollingFlightPlan,
|
||||
TarCapFlightPlan,
|
||||
)
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.displayoptions import DisplayOptions, ThreatZoneOptions
|
||||
@ -865,33 +863,6 @@ class QLiberationMap(QGraphicsView):
|
||||
if DisplayOptions.lines:
|
||||
self.draw_supply_route_between(cp, connected)
|
||||
|
||||
def _transfers_between(
|
||||
self, a: ControlPoint, b: ControlPoint
|
||||
) -> List[RoadTransferOrder]:
|
||||
# We attempt to short circuit the expensive shortest path computation for the
|
||||
# cases where there is never a transfer, but caching might be needed.
|
||||
|
||||
if a.captured != b.captured:
|
||||
# Cannot transfer to enemy CPs.
|
||||
return []
|
||||
|
||||
# This is only called for drawing lines between nodes and have rules out routes
|
||||
# to enemy bases, so a and b are guaranteed to be in the same supply route.
|
||||
supply_route = SupplyRoute.for_control_point(a)
|
||||
|
||||
transfers = []
|
||||
points = {a, b}
|
||||
for transfer in self.game.transfers:
|
||||
# No possible route from our network to this transfer.
|
||||
if transfer.position not in supply_route:
|
||||
continue
|
||||
|
||||
# Anything left is a transfer within our supply route.
|
||||
transfer_points = {transfer.position, transfer.next_stop()}
|
||||
if points == transfer_points:
|
||||
transfers.append(transfer)
|
||||
return transfers
|
||||
|
||||
def draw_supply_route_between(self, a: ControlPoint, b: ControlPoint) -> None:
|
||||
scene = self.scene()
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ from dcs.unittype import UnitType
|
||||
|
||||
from game import Game, db
|
||||
from game.theater import ControlPoint, SupplyRoute
|
||||
from game.transfers import AirliftPlanner, RoadTransferOrder
|
||||
from game.transfers import TransferOrder
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
|
||||
@ -89,50 +89,12 @@ class UnitTransferList(QFrame):
|
||||
main_layout.addWidget(scroll)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirliftCapacity:
|
||||
helicopter: int
|
||||
cargo_plane: int
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return self.helicopter + self.cargo_plane
|
||||
|
||||
@classmethod
|
||||
def to_control_point(cls, game: Game) -> AirliftCapacity:
|
||||
helo_capacity = 0
|
||||
plane_capacity = 0
|
||||
for cp in game.theater.player_points():
|
||||
inventory = game.aircraft_inventory.for_control_point(cp)
|
||||
for unit_type, count in inventory.all_aircraft:
|
||||
if unit_type.helicopter:
|
||||
helo_capacity += count
|
||||
return AirliftCapacity(helicopter=helo_capacity, cargo_plane=plane_capacity)
|
||||
|
||||
|
||||
class TransferOptionsPanel(QVBoxLayout):
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
origin: ControlPoint,
|
||||
airlift_capacity: AirliftCapacity,
|
||||
airlift_required: bool,
|
||||
) -> None:
|
||||
def __init__(self, game: Game, origin: ControlPoint) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.source_combo_box = TransferDestinationComboBox(game, origin)
|
||||
self.addLayout(QLabeledWidget("Destination:", self.source_combo_box))
|
||||
self.airlift = QCheckBox()
|
||||
self.airlift.setChecked(airlift_required)
|
||||
self.airlift.setDisabled(airlift_required)
|
||||
self.addLayout(QLabeledWidget("Airlift:", self.airlift))
|
||||
self.addWidget(
|
||||
QLabel(
|
||||
f"{airlift_capacity.total} airlift capacity "
|
||||
f"({airlift_capacity.cargo_plane} from cargo planes, "
|
||||
f"{airlift_capacity.helicopter} from helicopters)"
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
@ -194,17 +156,9 @@ class TransferControls(QGroupBox):
|
||||
|
||||
|
||||
class ScrollingUnitTransferGrid(QFrame):
|
||||
def __init__(
|
||||
self,
|
||||
cp: ControlPoint,
|
||||
airlift: bool,
|
||||
airlift_capacity: AirliftCapacity,
|
||||
game_model: GameModel,
|
||||
) -> None:
|
||||
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
self.cp = cp
|
||||
self.airlift = airlift
|
||||
self.remaining_capacity = airlift_capacity.total
|
||||
self.game_model = game_model
|
||||
self.transfers: Dict[Type[UnitType, int]] = defaultdict(int)
|
||||
|
||||
@ -274,11 +228,6 @@ class ScrollingUnitTransferGrid(QFrame):
|
||||
if not origin_inventory:
|
||||
return
|
||||
|
||||
if self.airlift:
|
||||
if not self.remaining_capacity:
|
||||
return
|
||||
self.remaining_capacity -= 1
|
||||
|
||||
self.transfers[unit_type] += 1
|
||||
origin_inventory -= 1
|
||||
controls.set_quantity(self.transfers[unit_type])
|
||||
@ -290,9 +239,6 @@ class ScrollingUnitTransferGrid(QFrame):
|
||||
if not controls.quantity:
|
||||
return
|
||||
|
||||
if self.airlift:
|
||||
self.remaining_capacity += 1
|
||||
|
||||
self.transfers[unit_type] -= 1
|
||||
origin_inventory += 1
|
||||
controls.set_quantity(self.transfers[unit_type])
|
||||
@ -329,21 +275,10 @@ class NewUnitTransferDialog(QDialog):
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
self.airlift_capacity = AirliftCapacity.to_control_point(game_model.game)
|
||||
airlift_required = len(SupplyRoute.for_control_point(origin)) == 1
|
||||
self.dest_panel = TransferOptionsPanel(
|
||||
game_model.game, origin, self.airlift_capacity, airlift_required
|
||||
)
|
||||
self.dest_panel.changed.connect(self.rebuild_transfers)
|
||||
self.dest_panel = TransferOptionsPanel(game_model.game, origin)
|
||||
layout.addLayout(self.dest_panel)
|
||||
|
||||
self.transfer_panel = ScrollingUnitTransferGrid(
|
||||
origin,
|
||||
airlift=airlift_required,
|
||||
airlift_capacity=self.airlift_capacity,
|
||||
game_model=game_model,
|
||||
)
|
||||
self.dest_panel.airlift.toggled.connect(self.rebuild_transfers)
|
||||
self.transfer_panel = ScrollingUnitTransferGrid(origin, game_model)
|
||||
layout.addWidget(self.transfer_panel)
|
||||
|
||||
self.submit_button = QPushButton("Create Transfer Order", parent=self)
|
||||
@ -351,31 +286,8 @@ class NewUnitTransferDialog(QDialog):
|
||||
self.submit_button.setProperty("style", "start-button")
|
||||
layout.addWidget(self.submit_button)
|
||||
|
||||
def rebuild_transfers(self) -> None:
|
||||
# Rebuild the transfer panel to reset everything. It's easier to recreate the
|
||||
# panel itself than to clear the grid layout in the panel.
|
||||
self.layout().removeWidget(self.transfer_panel)
|
||||
self.layout().removeWidget(self.submit_button)
|
||||
self.transfer_panel = ScrollingUnitTransferGrid(
|
||||
self.origin,
|
||||
airlift=self.dest_panel.airlift.isChecked(),
|
||||
airlift_capacity=self.airlift_capacity,
|
||||
game_model=self.game_model,
|
||||
)
|
||||
self.layout().addWidget(self.transfer_panel)
|
||||
self.layout().addWidget(self.submit_button)
|
||||
|
||||
def on_submit(self) -> None:
|
||||
destination = self.dest_panel.current
|
||||
supply_route = SupplyRoute.for_control_point(self.origin)
|
||||
if not self.dest_panel.airlift.isChecked() and destination not in supply_route:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Could not create transfer",
|
||||
f"Transfers from {self.origin} to {destination} require airlift.",
|
||||
QMessageBox.Ok,
|
||||
)
|
||||
return
|
||||
transfers = {}
|
||||
for unit_type, count in self.transfer_panel.transfers.items():
|
||||
if not count:
|
||||
@ -387,20 +299,10 @@ class NewUnitTransferDialog(QDialog):
|
||||
)
|
||||
transfers[unit_type] = count
|
||||
|
||||
if self.dest_panel.airlift.isChecked():
|
||||
planner = AirliftPlanner(
|
||||
self.game_model.game,
|
||||
self.origin,
|
||||
destination,
|
||||
transfers,
|
||||
)
|
||||
planner.create_package_for_airlift()
|
||||
else:
|
||||
transfer = RoadTransferOrder(
|
||||
player=True,
|
||||
origin=self.origin,
|
||||
destination=destination,
|
||||
units=transfers,
|
||||
)
|
||||
self.game_model.transfer_model.new_transfer(transfer)
|
||||
transfer = TransferOrder(
|
||||
origin=self.origin,
|
||||
destination=destination,
|
||||
units=transfers,
|
||||
)
|
||||
self.game_model.transfer_model.new_transfer(transfer)
|
||||
self.close()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user