From e5f4974e9ae6e36632566042d563ef821a5feb9f Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 3 Mar 2022 00:38:52 -0800 Subject: [PATCH] Stop ad-hoc constructing FrontLines. The UI needs to be able to identify these to the server and vice versa, so they'll need IDs that don't change. Rather than constructing an ID based on the control points names, make them an owned part of the control point. The constructed ID would be fine, but a UUID will make them more suitable for the database, and this was always fairly gross anyway. Some follow up work if anyone is interested: a bunch of the data that's computed in the various properties can now probably be computed *once* and persisted to the FrontLine type. --- game/game.py | 3 ++ game/missiongenerator/convoygenerator.py | 3 +- game/theater/conflicttheater.py | 7 +-- game/theater/controlpoint.py | 55 +++++++++++++++++------- game/theater/frontline.py | 17 +++++++- qt_ui/windows/basemenu/QBaseMenu2.py | 6 ++- 6 files changed, 65 insertions(+), 26 deletions(-) diff --git a/game/game.py b/game/game.py index 4cea586c..734b3ff1 100644 --- a/game/game.py +++ b/game/game.py @@ -273,6 +273,9 @@ class Game: """Initialization for the first turn of the game.""" from .sim import GameUpdateEvents + for control_point in self.theater.controlpoints: + control_point.initialize_turn_0() + self.blue.preinit_turn_0() self.red.preinit_turn_0() # We don't need to actually stream events for turn zero because we haven't given diff --git a/game/missiongenerator/convoygenerator.py b/game/missiongenerator/convoygenerator.py index a9f38df7..086c8b92 100644 --- a/game/missiongenerator/convoygenerator.py +++ b/game/missiongenerator/convoygenerator.py @@ -10,7 +10,6 @@ from dcs.unit import Vehicle from dcs.unitgroup import VehicleGroup from game.dcs.groundunittype import GroundUnitType -from game.theater import FrontLine from game.transfers import Convoy from game.unitmap import UnitMap from game.utils import kph @@ -46,7 +45,7 @@ class ConvoyGenerator: # convoys_travel_full_distance is disabled, so have the convoy only move the first segment on the route. # This option aims to remove long routes for ground vehicles between control points, # since the CPU load for pathfinding long routes on DCS can be pretty heavy. - frontline = FrontLine(convoy.origin, convoy.destination) + frontline = convoy.origin.front_line_with(convoy.destination) # Select the first route segment from the origin towards the destination # so the convoy spawns at the origin CP. This allows the convoy to be diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 240bc38f..775d010b 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -137,11 +137,8 @@ class ConflictTheater: return list(self.control_points_for(player=True)) def conflicts(self) -> Iterator[FrontLine]: - for player_cp in [x for x in self.controlpoints if x.captured]: - for enemy_cp in [ - x for x in player_cp.connected_points if not x.is_friendly_to(player_cp) - ]: - yield FrontLine(player_cp, enemy_cp) + for cp in self.player_points(): + yield from cp.front_lines.values() def enemy_points(self) -> List[ControlPoint]: return list(self.control_points_for(player=False)) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index d96b0748..621d0ac0 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -43,6 +43,7 @@ from game.sidc import ( ) from game.utils import Heading from .base import Base +from .frontline import FrontLine from .missiontarget import MissionTarget from .theatergroundobject import ( GenericCarrierGroundObject, @@ -62,7 +63,7 @@ if TYPE_CHECKING: from game.squadrons.squadron import Squadron from ..coalition import Coalition from ..transfers import PendingTransfers - from . import ConflictTheater + from .conflicttheater import ConflictTheater FREE_FRONTLINE_UNIT_SUPPLY: int = 15 AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12 @@ -287,15 +288,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): # the distance of the circle on the map. CAPTURE_DISTANCE = nautical_miles(2) - position = None # type: Point - name = None # type: str - - has_frontline = True - - alt = 0 - # TODO: Only airbases have IDs. - # TODO: has_frontline is only reasonable for airbases. # TODO: cptype is obsolete. def __init__( self, @@ -304,7 +297,6 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): position: Point, at: StartingPosition, starts_blue: bool, - has_frontline: bool = True, cptype: ControlPointType = ControlPointType.AIRBASE, ) -> None: super().__init__(name, position) @@ -319,8 +311,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): self._coalition: Optional[Coalition] = None self.captured_invert = False + self.front_lines: dict[ControlPoint, FrontLine] = {} # TODO: Should be Airbase specific. - self.has_frontline = has_frontline self.connected_points: List[ControlPoint] = [] self.convoy_routes: Dict[ControlPoint, Tuple[Point, ...]] = {} self.shipping_lanes: Dict[ControlPoint, Tuple[Point, ...]] = {} @@ -347,6 +339,41 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): assert self._coalition is None self._coalition = game.coalition_for(self.starts_blue) + def initialize_turn_0(self) -> None: + self._recreate_front_lines() + + def _recreate_front_lines(self) -> None: + self._clear_front_lines() + for connection in self.convoy_routes.keys(): + if not connection.front_line_active_with( + self + ) and not connection.is_friendly_to(self): + self._create_front_line_with(connection) + + def _create_front_line_with(self, connection: ControlPoint) -> None: + blue, red = FrontLine.sort_control_points(self, connection) + front = FrontLine(blue, red) + self.front_lines[connection] = front + connection.front_lines[self] = front + + def _remove_front_line_with(self, connection: ControlPoint) -> None: + del self.front_lines[connection] + del connection.front_lines[self] + + def _clear_front_lines(self) -> None: + for opponent in list(self.front_lines.keys()): + self._remove_front_line_with(opponent) + + @property + def has_frontline(self) -> bool: + return bool(self.front_lines) + + def front_line_active_with(self, other: ControlPoint) -> bool: + return other in self.front_lines + + def front_line_with(self, other: ControlPoint) -> FrontLine: + return self.front_lines[other] + @property def captured(self) -> bool: return self.coalition.player @@ -695,6 +722,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): self._coalition = new_coalition self.base.set_strength_to_minimum() + self._recreate_front_lines() @property def required_aircraft_start_type(self) -> Optional[StartType]: @@ -894,7 +922,6 @@ class Airfield(ControlPoint): airport.position, airport, starts_blue, - has_frontline=True, cptype=ControlPointType.AIRBASE, ) self.airport = airport @@ -1083,7 +1110,6 @@ class Carrier(NavalControlPoint): at, at, starts_blue, - has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP, ) @@ -1128,7 +1154,6 @@ class Lha(NavalControlPoint): at, at, starts_blue, - has_frontline=False, cptype=ControlPointType.LHA_GROUP, ) @@ -1166,7 +1191,6 @@ class OffMapSpawn(ControlPoint): position, position, starts_blue, - has_frontline=False, cptype=ControlPointType.OFF_MAP, ) @@ -1231,7 +1255,6 @@ class Fob(ControlPoint): at, at, starts_blue, - has_frontline=True, cptype=ControlPointType.FOB, ) self.name = name diff --git a/game/theater/frontline.py b/game/theater/frontline.py index 4a3ab51a..a17030c9 100644 --- a/game/theater/frontline.py +++ b/game/theater/frontline.py @@ -2,15 +2,16 @@ from __future__ import annotations import logging from dataclasses import dataclass -from typing import Iterator, List, Tuple, Any, TYPE_CHECKING +from typing import Any, Iterator, List, TYPE_CHECKING, Tuple from dcs.mapping import Point -from .controlpoint import ControlPoint, MissionTarget +from .missiontarget import MissionTarget from ..utils import Heading, pairwise if TYPE_CHECKING: from game.ato import FlightType + from .controlpoint import ControlPoint FRONTLINE_MIN_CP_DISTANCE = 5000 @@ -193,3 +194,15 @@ class FrontLine(MissionTarget): ): distance = FRONTLINE_MIN_CP_DISTANCE return distance + + @staticmethod + def sort_control_points( + a: ControlPoint, b: ControlPoint + ) -> tuple[ControlPoint, ControlPoint]: + if a.is_friendly_to(b): + raise ValueError( + "Cannot sort control points that are friendly to each other" + ) + if a.captured: + return a, b + return b, a diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index bee42a62..9cc55945 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -13,6 +13,8 @@ from PySide2.QtWidgets import ( from game import Game from game.ato.flighttype import FlightType from game.config import RUNWAY_REPAIR_COST +from game.server import EventStream +from game.sim import GameUpdateEvents from game.theater import ( AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION, ControlPoint, @@ -125,7 +127,9 @@ class QBaseMenu2(QDialog): self.cp.capture(self.game_model.game, for_player=not self.cp.captured) # Reinitialized ground planners and the like. The ATO needs to be reset because # missions planned against the flipped base are no longer valid. - self.game_model.game.initialize_turn() + events = GameUpdateEvents() + self.game_model.game.initialize_turn(events) + EventStream.put_nowait(events) GameUpdateSignal.get_instance().updateGame(self.game_model.game) @property