From d80f7ebf3ba23cd4594e7d6132d77ecd89e9a416 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 23 Apr 2021 18:40:31 -0700 Subject: [PATCH] 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 --- game/transfers.py | 361 ++++++++---------- game/unitdelivery.py | 44 +-- game/unitmap.py | 13 +- gen/aircraft.py | 37 +- gen/convoys.py | 2 +- gen/flights/flight.py | 4 +- qt_ui/models.py | 12 +- qt_ui/widgets/map/QLiberationMap.py | 33 +- .../windows/basemenu/NewUnitTransferDialog.py | 120 +----- 9 files changed, 227 insertions(+), 399 deletions(-) diff --git a/game/transfers.py b/game/transfers.py index d45f7832..4236a7aa 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -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." - ) diff --git a/game/unitdelivery.py b/game/unitdelivery.py index 845eec69..402a25cd 100644 --- a/game/unitdelivery.py +++ b/game/unitdelivery.py @@ -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 diff --git a/game/unitmap.py b/game/unitmap.py index 322320b7..c6d8d0de 100644 --- a/game/unitmap.py +++ b/game/unitmap.py @@ -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) diff --git a/gen/aircraft.py b/gen/aircraft.py index 174b2c8f..104cb6d9 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -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) diff --git a/gen/convoys.py b/gen/convoys.py index 21d48c8f..aa0e3e3d 100644 --- a/gen/convoys.py +++ b/gen/convoys.py @@ -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 diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 00b7523b..bab73cb4 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -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 diff --git a/qt_ui/models.py b/qt_ui/models.py index 7317f8f9..b07e7020 100644 --- a/qt_ui/models.py +++ b/qt_ui/models.py @@ -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() diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 3d54b651..b01504e1 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -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() diff --git a/qt_ui/windows/basemenu/NewUnitTransferDialog.py b/qt_ui/windows/basemenu/NewUnitTransferDialog.py index 9e2b5af4..591d8a07 100644 --- a/qt_ui/windows/basemenu/NewUnitTransferDialog.py +++ b/qt_ui/windows/basemenu/NewUnitTransferDialog.py @@ -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()