Add shipping lane support to campaign files.

These don't actually do anything yet, this is just the campaign support
and UI.

https://github.com/Khopa/dcs_liberation/issues/826
This commit is contained in:
Dan Albert 2021-04-24 21:32:37 -07:00
parent 97b73e1a01
commit 5e67ce0ab2
6 changed files with 173 additions and 40 deletions

View File

@ -5,12 +5,8 @@ import json
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
from itertools import tee
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast from typing import Any, Dict, Iterator, List, Optional, Tuple, Union, cast
from shapely import geometry
from shapely import ops
from dcs import Mission from dcs import Mission
from dcs.countries import ( from dcs.countries import (
@ -21,9 +17,10 @@ from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import F_15C from dcs.planes import F_15C
from dcs.ships import ( from dcs.ships import (
Bulker_Handy_Wind,
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
LHA_1_Tarawa,
DDG_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
LHA_1_Tarawa,
) )
from dcs.statics import Fortification from dcs.statics import Fortification
from dcs.terrain import ( from dcs.terrain import (
@ -43,20 +40,21 @@ from dcs.unitgroup import (
VehicleGroup, VehicleGroup,
) )
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from shapely import geometry, ops
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from .controlpoint import ( from .controlpoint import (
Airfield, Airfield,
Carrier, Carrier,
ControlPoint, ControlPoint,
Fob,
Lha, Lha,
MissionTarget, MissionTarget,
OffMapSpawn, OffMapSpawn,
Fob,
) )
from .landmap import Landmap, load_landmap, poly_contains from .landmap import Landmap, load_landmap, poly_contains
from ..point_with_heading import PointWithHeading from ..point_with_heading import PointWithHeading
from ..utils import Distance, meters, nautical_miles from ..utils import Distance, meters, nautical_miles, pairwise
Numeric = Union[int, float] Numeric = Union[int, float]
@ -73,16 +71,6 @@ IMPORTANCE_HIGH = 1.4
FRONTLINE_MIN_CP_DISTANCE = 5000 FRONTLINE_MIN_CP_DISTANCE = 5000
def pairwise(iterable):
"""
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
class MizCampaignLoader: class MizCampaignLoader:
BLUE_COUNTRY = CombinedJointTaskForcesBlue() BLUE_COUNTRY = CombinedJointTaskForcesBlue()
RED_COUNTRY = CombinedJointTaskForcesRed() RED_COUNTRY = CombinedJointTaskForcesRed()
@ -92,6 +80,7 @@ class MizCampaignLoader:
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
LHA_UNIT_TYPE = LHA_1_Tarawa.id LHA_UNIT_TYPE = LHA_1_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
SHIPPING_LANE_UNIT_TYPE = Bulker_Handy_Wind.id
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
FARP_HELIPAD = "SINGLE_HELIPAD" FARP_HELIPAD = "SINGLE_HELIPAD"
@ -315,6 +304,12 @@ class MizCampaignLoader:
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE: if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
yield group yield group
@property
def shipping_lane_groups(self) -> Iterator[ShipGroup]:
for group in self.country(blue=True).ship_group:
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
yield group
@cached_property @cached_property
def front_lines(self) -> Dict[str, ComplexFrontLine]: def front_lines(self) -> Dict[str, ComplexFrontLine]:
# Dict of front line ID to a front line. # Dict of front line ID to a front line.
@ -351,6 +346,28 @@ class MizCampaignLoader:
) )
return front_lines return front_lines
def add_shipping_lanes(self) -> None:
for group in self.shipping_lane_groups:
# The unit will have its first waypoint at the source CP and the final
# waypoint at the destination CP. Each waypoint defines the path of the
# cargo ship.
waypoints = [p.position for p in group.points]
origin = self.theater.closest_control_point(waypoints[0])
if origin is None:
raise RuntimeError(
f"No control point near the first waypoint of {group.name}"
)
destination = self.theater.closest_control_point(waypoints[-1])
if destination is None:
raise RuntimeError(
f"No control point near the final waypoint of {group.name}"
)
self.control_points[origin.id].create_shipping_lane(destination, waypoints)
self.control_points[destination.id].create_shipping_lane(
origin, list(reversed(waypoints))
)
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]: def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
closest = self.theater.closest_control_point(group.position) closest = self.theater.closest_control_point(group.position)
distance = meters(closest.position.distance_to_point(group.position)) distance = meters(closest.position.distance_to_point(group.position))
@ -446,6 +463,7 @@ class MizCampaignLoader:
for control_point in self.control_points.values(): for control_point in self.control_points.values():
self.theater.add_controlpoint(control_point) self.theater.add_controlpoint(control_point)
self.add_preset_locations() self.add_preset_locations()
self.add_shipping_lanes()
self.theater.set_frontline_data(self.front_lines) self.theater.set_frontline_data(self.front_lines)
@ -877,6 +895,12 @@ class FrontLine(MissionTarget):
""" """
return self.point_from_a(self._position_distance) return self.point_from_a(self._position_distance)
@property
def points(self) -> Iterator[Point]:
yield self.segments[0].point_a
for segment in self.segments:
yield segment.point_b
@property @property
def control_points(self) -> Tuple[ControlPoint, ControlPoint]: def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""Returns a tuple of the two control points.""" """Returns a tuple of the two control points."""

View File

@ -267,6 +267,7 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.has_frontline = has_frontline self.has_frontline = has_frontline
self.connected_points: List[ControlPoint] = [] self.connected_points: List[ControlPoint] = []
self.shipping_lanes: Dict[ControlPoint, List[Point]] = {}
self.convoy_spawns: Dict[ControlPoint, Point] = {} self.convoy_spawns: Dict[ControlPoint, Point] = {}
self.base: Base = Base() self.base: Base = Base()
self.cptype = cptype self.cptype = cptype
@ -397,6 +398,9 @@ class ControlPoint(MissionTarget, ABC):
self.convoy_spawns[to] = convoy_location self.convoy_spawns[to] = convoy_location
self.stances[to.id] = CombatStance.DEFENSIVE self.stances[to.id] = CombatStance.DEFENSIVE
def create_shipping_lane(self, to: ControlPoint, waypoints: List[Point]) -> None:
self.shipping_lanes[to] = waypoints
@abstractmethod @abstractmethod
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
""" """

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import itertools
import math import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import Union from typing import Union
@ -178,3 +179,13 @@ def mach(value: float, altitude: Distance) -> Speed:
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5) SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
def pairwise(iterable):
"""
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = itertools.tee(iterable)
next(b, None)
return zip(a, b)

View File

@ -39,4 +39,9 @@ VERSION = _build_version_string()
#: * Factories (Workshop_A) define factory objectives. Only control points with #: * Factories (Workshop_A) define factory objectives. Only control points with
#: factories will be able to recruit ground units, so they should exist in sufficient #: factories will be able to recruit ground units, so they should exist in sufficient
#: number and be protected by IADS. #: number and be protected by IADS.
CAMPAIGN_FORMAT_VERSION = 2 #:
#: Version 3
#: * Bulker Handy Winds define shipping lanes. They should be placed in port areas that
#: are navigable by ships and have a route to another port area. DCS ships *will not*
#: avoid driving into islands, so ensure that their waypoints plot a navigable route.
CAMPAIGN_FORMAT_VERSION = 3

View File

@ -45,7 +45,7 @@ from game.theater.theatergroundobject import (
TheaterGroundObject, TheaterGroundObject,
) )
from game.transfers import Convoy from game.transfers import Convoy
from game.utils import Distance, meters, nautical_miles from game.utils import Distance, meters, nautical_miles, pairwise
from game.weather import TimeOfDay from game.weather import TimeOfDay
from gen import Conflict, Package from gen import Conflict, Package
from gen.flights.flight import ( from gen.flights.flight import (
@ -67,12 +67,16 @@ from qt_ui.widgets.map.QFrontLine import QFrontLine
from qt_ui.widgets.map.QLiberationScene import QLiberationScene from qt_ui.widgets.map.QLiberationScene import QLiberationScene
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
from qt_ui.widgets.map.ShippingLaneSegment import ShippingLaneSegment
from qt_ui.widgets.map.SupplyRouteSegment import SupplyRouteSegment from qt_ui.widgets.map.SupplyRouteSegment import SupplyRouteSegment
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
MAX_SHIP_DISTANCE = nautical_miles(80) MAX_SHIP_DISTANCE = nautical_miles(80)
MapPoint = Tuple[float, float]
def binomial(i: int, n: int) -> float: def binomial(i: int, n: int) -> float:
"""Binomial coefficient""" """Binomial coefficient"""
return math.factorial(n) / float(math.factorial(i) * math.factorial(n - i)) return math.factorial(n) / float(math.factorial(i) * math.factorial(n - i))
@ -821,47 +825,64 @@ class QLiberationMap(QGraphicsView):
) )
) )
def bezier_points(
self, points: Iterable[Point]
) -> Iterator[Tuple[MapPoint, MapPoint]]:
# Thanks to Alquimista for sharing a python implementation of the bezier
# algorithm this is adapted from.
# https://gist.github.com/Alquimista/1274149#file-bezdraw-py
bezier_fixed_points = []
for a, b in pairwise(points):
bezier_fixed_points.append(self._transform_point(a))
bezier_fixed_points.append(self._transform_point(b))
old_point = bezier_fixed_points[0]
for point in bezier_curve_range(
int(len(bezier_fixed_points) * 2), bezier_fixed_points
):
yield old_point, point
old_point = point
def draw_bezier_frontline( def draw_bezier_frontline(
self, self,
scene: QGraphicsScene, scene: QGraphicsScene,
frontline: FrontLine, frontline: FrontLine,
convoys: List[Convoy], convoys: List[Convoy],
) -> None: ) -> None:
""" for a, b in self.bezier_points(frontline.points):
Thanks to Alquimista for sharing a python implementation of the bezier algorithm this is adapted from.
https://gist.github.com/Alquimista/1274149#file-bezdraw-py
"""
bezier_fixed_points = []
for segment in frontline.segments:
bezier_fixed_points.append(self._transform_point(segment.point_a))
bezier_fixed_points.append(self._transform_point(segment.point_b))
old_point = bezier_fixed_points[0]
for point in bezier_curve_range(
int(len(bezier_fixed_points) * 2), bezier_fixed_points
):
scene.addItem( scene.addItem(
SupplyRouteSegment( SupplyRouteSegment(
old_point[0], a[0],
old_point[1], a[1],
point[0], b[0],
point[1], b[1],
frontline.control_point_a, frontline.control_point_a,
frontline.control_point_b, frontline.control_point_b,
convoys, convoys,
) )
) )
old_point = point
def draw_supply_routes(self) -> None: def draw_supply_routes(self) -> None:
if not DisplayOptions.lines:
return
seen = set() seen = set()
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
seen.add(cp) seen.add(cp)
for connected in cp.connected_points: for connected in cp.connected_points:
if connected in seen: if connected in seen:
continue continue
if DisplayOptions.lines: self.draw_supply_route_between(cp, connected)
self.draw_supply_route_between(cp, connected) for destination, shipping_lane in cp.shipping_lanes.items():
if destination in seen:
continue
if cp.is_friendly(destination.captured):
self.draw_shipping_lane_between(cp, destination)
def draw_shipping_lane_between(self, a: ControlPoint, b: ControlPoint) -> None:
scene = self.scene()
for pa, pb in self.bezier_points(a.shipping_lanes[b]):
scene.addItem(ShippingLaneSegment(pa[0], pa[1], pb[0], pb[1], a, b))
def draw_supply_route_between(self, a: ControlPoint, b: ControlPoint) -> None: def draw_supply_route_between(self, a: ControlPoint, b: ControlPoint) -> None:
scene = self.scene() scene = self.scene()

View File

@ -0,0 +1,68 @@
from typing import Optional
from PySide2.QtCore import Qt
from PySide2.QtGui import QColor, QPen
from PySide2.QtWidgets import (
QGraphicsItem,
QGraphicsLineItem,
)
from game.theater import ControlPoint
from qt_ui.uiconstants import COLORS
class ShippingLaneSegment(QGraphicsLineItem):
def __init__(
self,
x0: float,
y0: float,
x1: float,
y1: float,
control_point_a: ControlPoint,
control_point_b: ControlPoint,
parent: Optional[QGraphicsItem] = None,
) -> None:
super().__init__(x0, y0, x1, y1, parent)
self.control_point_a = control_point_a
self.control_point_b = control_point_b
self.ships = []
self.setPen(self.make_pen())
self.setToolTip(self.make_tooltip())
self.setAcceptHoverEvents(True)
@property
def has_ships(self) -> bool:
return bool(self.ships)
def make_tooltip(self) -> str:
if not self.has_ships:
return "No ships present in this shipping lane."
ships = []
for ship in self.ships:
units = "units" if ship.size > 1 else "unit"
ships.append(
f"{ship.size} {units} transferring from {ship.origin} to "
f"{ship.destination}."
)
return "\n".join(ships)
@property
def line_color(self) -> QColor:
if self.control_point_a.captured:
return COLORS["dark_blue"]
else:
return COLORS["dark_red"]
@property
def line_style(self) -> Qt.PenStyle:
if self.has_ships:
return Qt.PenStyle.SolidLine
return Qt.PenStyle.DotLine
def make_pen(self) -> QPen:
pen = QPen(brush=self.line_color)
pen.setColor(self.line_color)
pen.setStyle(self.line_style)
pen.setWidth(2)
return pen