mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
First pass at cargo ships.
The simple form of this works, but without the multi-mode routing it'll only get used when the final destination is a port with a link to a port with a factory. These also aren't targetable or simulated yet. https://github.com/Khopa/dcs_liberation/issues/826
This commit is contained in:
@@ -315,6 +315,23 @@ class ControlPoint(MissionTarget, ABC):
|
||||
connected.extend(cp.transitive_connected_friendly_points(seen))
|
||||
return connected
|
||||
|
||||
def transitive_friendly_shipping_destinations(
|
||||
self, seen: Optional[Set[ControlPoint]] = None
|
||||
) -> List[ControlPoint]:
|
||||
if seen is None:
|
||||
seen = {self}
|
||||
|
||||
connected = []
|
||||
for cp in self.shipping_lanes:
|
||||
if cp.captured != self.captured:
|
||||
continue
|
||||
if cp in seen:
|
||||
continue
|
||||
seen.add(cp)
|
||||
connected.append(cp)
|
||||
connected.extend(cp.transitive_friendly_shipping_destinations(seen))
|
||||
return connected
|
||||
|
||||
@property
|
||||
def has_factory(self) -> bool:
|
||||
for tgo in self.connected_objectives:
|
||||
|
||||
@@ -4,7 +4,7 @@ import heapq
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Iterator, List, Optional
|
||||
from typing import Dict, Iterable, Iterator, List, Optional
|
||||
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
|
||||
@@ -32,6 +32,25 @@ class Frontier:
|
||||
return bool(self.nodes)
|
||||
|
||||
|
||||
# TODO: Build a single SupplyRoute for each coalition at the start of the turn.
|
||||
# Supply routes need to cover the whole network to support multi-mode links.
|
||||
#
|
||||
# Traverse each friendly control point and build out a network from each. Nodes create
|
||||
# connections to:
|
||||
#
|
||||
# 1. Bases connected by road
|
||||
# 2. Bases connected by rail
|
||||
# 3. Bases connected by shipping lane
|
||||
# 4. Airports large enough to operate cargo planes connect to each other
|
||||
# 5. Airports capable of operating helicopters connect to other airports within cargo
|
||||
# helicopter range, and FOBs within half of the range (since they can't be refueled
|
||||
# at the drop off).
|
||||
#
|
||||
# The costs of each link would be set such that the above order roughly corresponds to
|
||||
# the prevalence of each type of transport. Most units should move by road, rail should
|
||||
# be used a little less often than road, ships a bit less often than that, cargo planes
|
||||
# infrequently, and helicopters rarely. Convoys, trains, and ships make the most
|
||||
# interesting targets for players (and the easiest to generate AI flight plans for).
|
||||
class SupplyRoute:
|
||||
def __init__(self, control_points: List[ControlPoint]) -> None:
|
||||
self.control_points = control_points
|
||||
@@ -45,20 +64,16 @@ class SupplyRoute:
|
||||
def __len__(self) -> int:
|
||||
return len(self.control_points)
|
||||
|
||||
@classmethod
|
||||
def for_control_point(cls, control_point: ControlPoint) -> SupplyRoute:
|
||||
connected_friendly_points = control_point.transitive_connected_friendly_points()
|
||||
if not connected_friendly_points:
|
||||
return SupplyRoute([control_point])
|
||||
return SupplyRoute([control_point] + connected_friendly_points)
|
||||
def connections_from(self, control_point: ControlPoint) -> Iterable:
|
||||
raise NotImplementedError
|
||||
|
||||
def shortest_path_between(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> List[ControlPoint]:
|
||||
if origin not in self:
|
||||
raise ValueError(f"{origin.name} is not in this supply route")
|
||||
raise ValueError(f"{origin} is not in supply route to {destination}")
|
||||
if destination not in self:
|
||||
raise ValueError(f"{destination.name} is not in this supply route")
|
||||
raise ValueError(f"{destination} is not in supply route from {origin}")
|
||||
|
||||
frontier = Frontier()
|
||||
frontier.push(origin, 0)
|
||||
@@ -74,7 +89,7 @@ class SupplyRoute:
|
||||
if cost > best_known[current]:
|
||||
continue
|
||||
|
||||
for neighbor in current.connected_points:
|
||||
for neighbor in self.connections_from(current):
|
||||
if current.captured != neighbor.captured:
|
||||
continue
|
||||
|
||||
@@ -97,3 +112,29 @@ class SupplyRoute:
|
||||
current = previous
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
|
||||
class RoadNetwork(SupplyRoute):
|
||||
@classmethod
|
||||
def for_control_point(cls, control_point: ControlPoint) -> RoadNetwork:
|
||||
connected_friendly_points = control_point.transitive_connected_friendly_points()
|
||||
if not connected_friendly_points:
|
||||
return RoadNetwork([control_point])
|
||||
return RoadNetwork([control_point] + connected_friendly_points)
|
||||
|
||||
def connections_from(self, control_point: ControlPoint) -> Iterable:
|
||||
yield from control_point.connected_points
|
||||
|
||||
|
||||
class ShippingNetwork(SupplyRoute):
|
||||
@classmethod
|
||||
def for_control_point(cls, control_point: ControlPoint) -> ShippingNetwork:
|
||||
connected_friendly_points = (
|
||||
control_point.transitive_friendly_shipping_destinations()
|
||||
)
|
||||
if not connected_friendly_points:
|
||||
return ShippingNetwork([control_point])
|
||||
return ShippingNetwork([control_point] + connected_friendly_points)
|
||||
|
||||
def connections_from(self, control_point: ControlPoint) -> Iterable:
|
||||
yield from control_point.shipping_lanes
|
||||
|
||||
@@ -16,7 +16,7 @@ from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.theater.supplyroutes import SupplyRoute
|
||||
from game.theater.supplyroutes import RoadNetwork, ShippingNetwork, SupplyRoute
|
||||
from gen.naming import namegen
|
||||
from gen.flights.flight import Flight, FlightType
|
||||
|
||||
@@ -242,20 +242,15 @@ class AirliftPlanner:
|
||||
return flight_size
|
||||
|
||||
|
||||
class Convoy(MissionTarget, Transport):
|
||||
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
||||
super().__init__(namegen.next_convoy_name(), origin.position)
|
||||
class MultiGroupTransport(MissionTarget, Transport):
|
||||
def __init__(
|
||||
self, name: str, origin: ControlPoint, destination: ControlPoint
|
||||
) -> None:
|
||||
super().__init__(name, origin.position)
|
||||
self.origin = origin
|
||||
self.destination = destination
|
||||
self.transfers: List[TransferOrder] = []
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
if self.is_friendly(for_player):
|
||||
return
|
||||
|
||||
yield FlightType.BAI
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
return self.origin.captured
|
||||
|
||||
@@ -298,10 +293,30 @@ class Convoy(MissionTarget, Transport):
|
||||
return self.origin.captured
|
||||
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
return None
|
||||
raise NotImplementedError
|
||||
|
||||
def description(self) -> str:
|
||||
return f"In a convoy to {self.destination}"
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def route_start(self) -> Point:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def route_end(self) -> Point:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Convoy(MultiGroupTransport):
|
||||
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
||||
super().__init__(namegen.next_convoy_name(), origin, destination)
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
if self.is_friendly(for_player):
|
||||
return
|
||||
|
||||
yield FlightType.BAI
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def route_start(self) -> Point:
|
||||
@@ -311,44 +326,85 @@ class Convoy(MissionTarget, Transport):
|
||||
def route_end(self) -> Point:
|
||||
return self.destination.convoy_spawns[self.origin]
|
||||
|
||||
def description(self) -> str:
|
||||
return f"In a convoy to {self.destination}"
|
||||
|
||||
class ConvoyMap:
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
return None
|
||||
|
||||
|
||||
class CargoShip(MultiGroupTransport):
|
||||
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
||||
super().__init__(namegen.next_cargo_ship_name(), origin, destination)
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
if self.is_friendly(for_player):
|
||||
return
|
||||
|
||||
yield FlightType.ANTISHIP
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def route_start(self) -> Point:
|
||||
return self.origin.shipping_lanes[self.destination][0]
|
||||
|
||||
@property
|
||||
def route_end(self) -> Point:
|
||||
return self.destination.shipping_lanes[self.origin][-1]
|
||||
|
||||
def description(self) -> str:
|
||||
return f"On a ship to {self.destination}"
|
||||
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
return None
|
||||
|
||||
|
||||
class TransportMap:
|
||||
def __init__(self) -> None:
|
||||
# Dict of origin -> destination -> convoy.
|
||||
self.convoys: Dict[ControlPoint, Dict[ControlPoint, Convoy]] = defaultdict(dict)
|
||||
# Dict of origin -> destination -> transport.
|
||||
self.transports: Dict[
|
||||
ControlPoint, Dict[ControlPoint, MultiGroupTransport]
|
||||
] = defaultdict(dict)
|
||||
|
||||
def convoy_exists(self, origin: ControlPoint, destination: ControlPoint) -> bool:
|
||||
return destination in self.convoys[origin]
|
||||
|
||||
def find_convoy(
|
||||
def create_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> Optional[Convoy]:
|
||||
return self.convoys[origin].get(destination)
|
||||
) -> MultiGroupTransport:
|
||||
raise NotImplementedError
|
||||
|
||||
def find_or_create_convoy(
|
||||
def transport_exists(self, origin: ControlPoint, destination: ControlPoint) -> bool:
|
||||
return destination in self.transports[origin]
|
||||
|
||||
def find_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> Convoy:
|
||||
convoy = self.find_convoy(origin, destination)
|
||||
if convoy is None:
|
||||
convoy = Convoy(origin, destination)
|
||||
self.convoys[origin][destination] = convoy
|
||||
return convoy
|
||||
) -> Optional[MultiGroupTransport]:
|
||||
return self.transports[origin].get(destination)
|
||||
|
||||
def departing_from(self, origin: ControlPoint) -> Iterator[Convoy]:
|
||||
yield from self.convoys[origin].values()
|
||||
def find_or_create_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> MultiGroupTransport:
|
||||
transport = self.find_transport(origin, destination)
|
||||
if transport is None:
|
||||
transport = self.create_transport(origin, destination)
|
||||
self.transports[origin][destination] = transport
|
||||
return transport
|
||||
|
||||
def travelling_to(self, destination: ControlPoint) -> Iterator[Convoy]:
|
||||
for destination_dict in self.convoys.values():
|
||||
def departing_from(self, origin: ControlPoint) -> Iterator[MultiGroupTransport]:
|
||||
yield from self.transports[origin].values()
|
||||
|
||||
def travelling_to(self, destination: ControlPoint) -> Iterator[MultiGroupTransport]:
|
||||
for destination_dict in self.transports.values():
|
||||
if destination in destination_dict:
|
||||
yield destination_dict[destination]
|
||||
|
||||
def disband_convoy(self, convoy: Convoy) -> None:
|
||||
self.convoys[convoy.origin][convoy.destination].disband()
|
||||
del self.convoys[convoy.origin][convoy.destination]
|
||||
def disband_transport(self, transport: MultiGroupTransport) -> None:
|
||||
transport.disband()
|
||||
del self.transports[transport.origin][transport.destination]
|
||||
|
||||
@staticmethod
|
||||
def path_for(transfer: TransferOrder) -> List[ControlPoint]:
|
||||
supply_route = SupplyRoute.for_control_point(transfer.position)
|
||||
def network_for(self, control_point: ControlPoint) -> SupplyRoute:
|
||||
raise NotImplementedError
|
||||
|
||||
def path_for(self, transfer: TransferOrder) -> List[ControlPoint]:
|
||||
supply_route = self.network_for(transfer.position)
|
||||
return supply_route.shortest_path_between(
|
||||
transfer.position, transfer.destination
|
||||
)
|
||||
@@ -358,26 +414,47 @@ class ConvoyMap:
|
||||
|
||||
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)
|
||||
self.find_or_create_transport(transfer.position, next_stop).add_units(transfer)
|
||||
|
||||
def remove(self, convoy: Convoy, transfer: TransferOrder) -> None:
|
||||
convoy.remove_units(transfer)
|
||||
if not convoy.transfers:
|
||||
self.disband_convoy(convoy)
|
||||
def remove(self, transport: MultiGroupTransport, transfer: TransferOrder) -> None:
|
||||
transport.remove_units(transfer)
|
||||
if not transport.transfers:
|
||||
self.disband_transport(transport)
|
||||
|
||||
def disband_all(self) -> None:
|
||||
for convoy in list(self):
|
||||
self.disband_convoy(convoy)
|
||||
for transport in list(self):
|
||||
self.disband_transport(transport)
|
||||
|
||||
def __iter__(self) -> Iterator[Convoy]:
|
||||
for destination_dict in self.convoys.values():
|
||||
def __iter__(self) -> Iterator[MultiGroupTransport]:
|
||||
for destination_dict in self.transports.values():
|
||||
yield from destination_dict.values()
|
||||
|
||||
|
||||
class ConvoyMap(TransportMap):
|
||||
def create_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> Convoy:
|
||||
return Convoy(origin, destination)
|
||||
|
||||
def network_for(self, control_point: ControlPoint) -> RoadNetwork:
|
||||
return RoadNetwork.for_control_point(control_point)
|
||||
|
||||
|
||||
class CargoShipMap(TransportMap):
|
||||
def create_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> CargoShip:
|
||||
return CargoShip(origin, destination)
|
||||
|
||||
def network_for(self, control_point: ControlPoint) -> ShippingNetwork:
|
||||
return ShippingNetwork.for_control_point(control_point)
|
||||
|
||||
|
||||
class PendingTransfers:
|
||||
def __init__(self, game: Game) -> None:
|
||||
self.game = game
|
||||
self.convoys = ConvoyMap()
|
||||
self.cargo_ships = CargoShipMap()
|
||||
self.pending_transfers: List[TransferOrder] = []
|
||||
|
||||
def __iter__(self) -> Iterator[TransferOrder]:
|
||||
@@ -394,9 +471,12 @@ class PendingTransfers:
|
||||
return self.pending_transfers.index(transfer)
|
||||
|
||||
def arrange_transport(self, transfer: TransferOrder) -> None:
|
||||
supply_route = SupplyRoute.for_control_point(transfer.position)
|
||||
if transfer.destination in supply_route:
|
||||
if transfer.destination in RoadNetwork.for_control_point(transfer.position):
|
||||
self.convoys.add(transfer)
|
||||
elif transfer.destination in ShippingNetwork.for_control_point(
|
||||
transfer.position
|
||||
):
|
||||
self.cargo_ships.add(transfer)
|
||||
else:
|
||||
AirliftPlanner(self.game, transfer).create_package_for_airlift()
|
||||
|
||||
@@ -439,6 +519,11 @@ class PendingTransfers:
|
||||
) -> None:
|
||||
self.convoys.remove(transport, transfer)
|
||||
|
||||
def _cancel_transport_cargo_ship(
|
||||
self, transfer: TransferOrder, transport: CargoShip
|
||||
) -> None:
|
||||
self.cargo_ships.remove(transport, transfer)
|
||||
|
||||
def cancel_transfer(self, transfer: TransferOrder) -> None:
|
||||
if transfer.transport is not None:
|
||||
self.cancel_transport(transfer, transfer.transport)
|
||||
|
||||
@@ -10,6 +10,7 @@ from dcs.unittype import UnitType, VehicleType
|
||||
from game.theater import ControlPoint, SupplyRoute
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from .db import PRICES
|
||||
from .theater.supplyroutes import RoadNetwork, ShippingNetwork
|
||||
from .transfers import TransferOrder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -19,7 +20,6 @@ if TYPE_CHECKING:
|
||||
@dataclass(frozen=True)
|
||||
class GroundUnitSource:
|
||||
control_point: ControlPoint
|
||||
requires_airlift: bool
|
||||
|
||||
|
||||
class PendingUnitDeliveries:
|
||||
@@ -84,9 +84,9 @@ class PendingUnitDeliveries:
|
||||
|
||||
if (
|
||||
issubclass(unit_type, VehicleType)
|
||||
and self.destination != ground_unit_source.control_point
|
||||
and self.destination != ground_unit_source
|
||||
):
|
||||
source = ground_unit_source.control_point
|
||||
source = ground_unit_source
|
||||
d = units_needing_transfer
|
||||
else:
|
||||
source = self.destination
|
||||
@@ -106,41 +106,45 @@ class PendingUnitDeliveries:
|
||||
self.destination.base.commit_losses(sold_units)
|
||||
|
||||
if units_needing_transfer:
|
||||
ground_unit_source.control_point.base.commision_units(
|
||||
units_needing_transfer
|
||||
)
|
||||
self.create_transfer(
|
||||
game, ground_unit_source.control_point, units_needing_transfer
|
||||
)
|
||||
ground_unit_source.base.commision_units(units_needing_transfer)
|
||||
self.create_transfer(game, ground_unit_source, units_needing_transfer)
|
||||
|
||||
def create_transfer(
|
||||
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
|
||||
) -> None:
|
||||
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
|
||||
|
||||
def find_ground_unit_source(self, game: Game) -> Optional[GroundUnitSource]:
|
||||
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
|
||||
# This is running *after* the turn counter has been incremented, so this is the
|
||||
# reaction to turn 0. On turn zero we allow units to be recruited anywhere for
|
||||
# delivery on turn 1 so that turn 1 always starts with units on the front line.
|
||||
if game.turn == 1:
|
||||
return GroundUnitSource(self.destination, requires_airlift=False)
|
||||
return self.destination
|
||||
|
||||
# Fast path if the destination is a valid source.
|
||||
if self.destination.can_recruit_ground_units(game):
|
||||
return GroundUnitSource(self.destination, requires_airlift=False)
|
||||
return self.destination
|
||||
|
||||
by_road = self.find_ground_unit_source_by_road(game)
|
||||
by_road = self.find_ground_unit_source_in_supply_route(
|
||||
RoadNetwork.for_control_point(self.destination), game
|
||||
)
|
||||
if by_road is not None:
|
||||
return GroundUnitSource(by_road, requires_airlift=False)
|
||||
return by_road
|
||||
|
||||
by_ship = self.find_ground_unit_source_in_supply_route(
|
||||
ShippingNetwork.for_control_point(self.destination), game
|
||||
)
|
||||
if by_ship is not None:
|
||||
return by_ship
|
||||
|
||||
by_air = self.find_ground_unit_source_by_air(game)
|
||||
if by_air is not None:
|
||||
return GroundUnitSource(by_air, requires_airlift=True)
|
||||
return by_air
|
||||
return None
|
||||
|
||||
def find_ground_unit_source_by_road(self, game: Game) -> Optional[ControlPoint]:
|
||||
supply_route = SupplyRoute.for_control_point(self.destination)
|
||||
|
||||
def find_ground_unit_source_in_supply_route(
|
||||
self, supply_route: SupplyRoute, game: Game
|
||||
) -> Optional[ControlPoint]:
|
||||
sources = []
|
||||
for control_point in supply_route:
|
||||
if control_point.can_recruit_ground_units(game):
|
||||
|
||||
@@ -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 Convoy, TransferOrder
|
||||
from game.transfers import MultiGroupTransport, TransferOrder
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class GroundObjectUnit:
|
||||
@dataclass(frozen=True)
|
||||
class ConvoyUnit:
|
||||
unit_type: Type[VehicleType]
|
||||
convoy: Convoy
|
||||
convoy: MultiGroupTransport
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -130,7 +130,7 @@ class UnitMap:
|
||||
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
|
||||
return self.ground_object_units.get(name, None)
|
||||
|
||||
def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
|
||||
def add_convoy_units(self, group: Group, convoy: MultiGroupTransport) -> None:
|
||||
for unit in group.units:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
|
||||
Reference in New Issue
Block a user