From 0e6a303c17b3f5cdd7f39721db265ca9fbf0d8a9 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 22 Feb 2022 18:47:51 -0800 Subject: [PATCH] Move NavMesh out of MapModel. --- game/coalition.py | 12 ++++--- game/game.py | 39 +++++++++++++++------ game/server/app.py | 3 +- game/server/dependencies.py | 9 +++-- game/server/eventstream/eventstream.py | 2 +- game/server/eventstream/models.py | 4 ++- game/server/navmesh/__init__.py | 1 + game/server/navmesh/models.py | 10 ++++++ game/server/navmesh/routes.py | 20 +++++++++++ game/sim/__init__.py | 1 + game/sim/gameupdateevents.py | 5 +++ qt_ui/widgets/map/model/mapmodel.py | 15 +------- qt_ui/widgets/map/model/navmeshjs.py | 44 ------------------------ qt_ui/widgets/map/model/navmeshpolyjs.py | 31 ----------------- resources/ui/map/map.js | 39 +++++++++++---------- 15 files changed, 108 insertions(+), 127 deletions(-) create mode 100644 game/server/navmesh/__init__.py create mode 100644 game/server/navmesh/models.py create mode 100644 game/server/navmesh/routes.py delete mode 100644 qt_ui/widgets/map/model/navmeshjs.py delete mode 100644 qt_ui/widgets/map/model/navmeshpolyjs.py diff --git a/game/coalition.py b/game/coalition.py index 5d49e037..52ddfc95 100644 --- a/game/coalition.py +++ b/game/coalition.py @@ -21,10 +21,11 @@ from game.threatzones import ThreatZones from game.transfers import PendingTransfers if TYPE_CHECKING: - from game import Game - from game.campaignloader import CampaignAirWingConfig - from game.data.doctrine import Doctrine - from game.factions.faction import Faction + from .campaignloader import CampaignAirWingConfig + from .data.doctrine import Doctrine + from .factions.faction import Faction + from .game import Game + from .sim import GameUpdateEvents class Coalition: @@ -121,10 +122,11 @@ class Coalition: def compute_threat_zones(self) -> None: self._threat_zone = ThreatZones.for_faction(self.game, self.player) - def compute_nav_meshes(self) -> None: + def compute_nav_meshes(self, events: GameUpdateEvents) -> None: self._navmesh = NavMesh.from_threat_zones( self.opponent.threat_zone, self.game.theater ) + events.update_navmesh(self.player) def update_transit_network(self) -> None: self.transit_network = TransitNetworkBuilder( diff --git a/game/game.py b/game/game.py index 84242a67..e2ee6379 100644 --- a/game/game.py +++ b/game/game.py @@ -15,11 +15,11 @@ from dcs.task import CAP, CAS, PinpointStrike from dcs.vehicles import AirDefence from faker import Faker +from game.ato.closestairfields import ObjectiveDistanceCache from game.ground_forces.ai_ground_planner import GroundPlanner from game.models.game_stats import GameStats from game.plugins import LuaPluginManager from game.utils import Distance -from game.ato.closestairfields import ObjectiveDistanceCache from . import naming, persistency from .ato.flighttype import FlightType from .campaignloader import CampaignAirWingConfig @@ -40,10 +40,11 @@ from .weather import Conditions, TimeOfDay if TYPE_CHECKING: from .ato.airtaaskingorder import AirTaskingOrder + from .factions.faction import Faction from .navmesh import NavMesh + from .sim import GameUpdateEvents from .squadrons import AirWing from .threatzones import ThreatZones - from .factions.faction import Faction COMMISION_UNIT_VARIETY = 4 COMMISION_LIMITS_SCALE = 1.5 @@ -203,6 +204,8 @@ class Game: self.coalition_for(player).adjust_budget(amount) def on_load(self, game_still_initializing: bool = False) -> None: + from .sim import GameUpdateEvents + if not hasattr(self, "name_generator"): self.name_generator = naming.namegen # Hack: Replace the global name generator state with the state from the save @@ -215,7 +218,9 @@ class Game: ObjectiveDistanceCache.set_theater(self.theater) self.compute_unculled_zones() if not game_still_initializing: - self.compute_threat_zones() + # We don't need to push events that happen during load. The UI will fully + # reset when we're done. + self.compute_threat_zones(GameUpdateEvents()) def finish_turn(self, skipped: bool = False) -> None: """Finalizes the current turn and advances to the next turn. @@ -266,9 +271,13 @@ class Game: def begin_turn_0(self) -> None: """Initialization for the first turn of the game.""" + from .sim import GameUpdateEvents + self.blue.preinit_turn_0() self.red.preinit_turn_0() - self.initialize_turn() + # We don't need to actually stream events for turn zero because we haven't given + # *any* state to the UI yet, so it will need to do a full draw once we do. + self.initialize_turn(GameUpdateEvents()) def pass_turn(self, no_action: bool = False) -> None: """Ends the current turn and initializes the new turn. @@ -278,11 +287,18 @@ class Game: Args: no_action: True if the turn was skipped. """ + from .server import EventStream + from .sim import GameUpdateEvents + logging.info("Pass turn") with logged_duration("Turn finalization"): self.finish_turn(no_action) + + events = GameUpdateEvents() with logged_duration("Turn initialization"): - self.initialize_turn() + self.initialize_turn(events) + + EventStream.put_nowait(events) # Autosave progress persistency.autosave(self) @@ -307,7 +323,9 @@ class Game: self.blue.bullseye = Bullseye(enemy_cp.position) self.red.bullseye = Bullseye(player_cp.position) - def initialize_turn(self, for_red: bool = True, for_blue: bool = True) -> None: + def initialize_turn( + self, events: GameUpdateEvents, for_red: bool = True, for_blue: bool = True + ) -> None: """Performs turn initialization for the specified players. Turn initialization performs all of the beginning-of-turn actions. *End-of-turn* @@ -338,6 +356,7 @@ class Game: impactful but also likely to be early, so they also cause a blue replan. Args: + events: Game update event container for turn initialization. for_red: True if opfor should be re-initialized. for_blue: True if the player coalition should be re-initialized. """ @@ -353,7 +372,7 @@ class Game: # Plan flights & combat for next turn with logged_duration("Threat zone computation"): - self.compute_threat_zones() + self.compute_threat_zones(events) # Plan Coalition specific turn if for_blue: @@ -401,11 +420,11 @@ class Game: def compute_transit_network_for(self, player: bool) -> TransitNetwork: return TransitNetworkBuilder(self.theater, player).build() - def compute_threat_zones(self) -> None: + def compute_threat_zones(self, events: GameUpdateEvents) -> None: self.blue.compute_threat_zones() self.red.compute_threat_zones() - self.blue.compute_nav_meshes() - self.red.compute_nav_meshes() + self.blue.compute_nav_meshes(events) + self.red.compute_nav_meshes(events) def threat_zone_for(self, player: bool) -> ThreatZones: return self.coalition_for(player).threat_zone diff --git a/game/server/app.py b/game/server/app.py index fe7c8fa1..1a09f0b1 100644 --- a/game/server/app.py +++ b/game/server/app.py @@ -1,10 +1,11 @@ from fastapi import Depends, FastAPI -from . import debuggeometries, eventstream, flights, waypoints +from . import debuggeometries, eventstream, flights, navmesh, waypoints from .security import ApiKeyManager app = FastAPI(dependencies=[Depends(ApiKeyManager.verify)]) app.include_router(debuggeometries.router) app.include_router(eventstream.router) app.include_router(flights.router) +app.include_router(navmesh.router) app.include_router(waypoints.router) diff --git a/game/server/dependencies.py b/game/server/dependencies.py index e93eb7ee..0440d44f 100644 --- a/game/server/dependencies.py +++ b/game/server/dependencies.py @@ -1,5 +1,10 @@ -from game import Game -from qt_ui.models import GameModel +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from game import Game + from qt_ui.models import GameModel class GameContext: diff --git a/game/server/eventstream/eventstream.py b/game/server/eventstream/eventstream.py index 96494720..38c71635 100644 --- a/game/server/eventstream/eventstream.py +++ b/game/server/eventstream/eventstream.py @@ -1,6 +1,6 @@ from asyncio import Queue -from game.sim.gameupdateevents import GameUpdateEvents +from game.sim import GameUpdateEvents class EventStream: diff --git a/game/server/eventstream/models.py b/game/server/eventstream/models.py index a083a1c9..e10532e7 100644 --- a/game/server/eventstream/models.py +++ b/game/server/eventstream/models.py @@ -10,13 +10,14 @@ from game.server.leaflet import LeafletLatLon if TYPE_CHECKING: from game import Game - from game.sim.gameupdateevents import GameUpdateEvents + from game.sim import GameUpdateEvents class GameUpdateEventsJs(BaseModel): updated_flights: dict[UUID, LeafletLatLon] new_combats: list[FrozenCombatJs] = [] updated_combats: list[FrozenCombatJs] = [] + navmesh_updates: set[bool] = set() @classmethod def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs: @@ -31,4 +32,5 @@ class GameUpdateEventsJs(BaseModel): FrozenCombatJs.for_combat(c, game.theater) for c in events.updated_combats ], + navmesh_updates=events.navmesh_updates, ) diff --git a/game/server/navmesh/__init__.py b/game/server/navmesh/__init__.py new file mode 100644 index 00000000..3a27ef1c --- /dev/null +++ b/game/server/navmesh/__init__.py @@ -0,0 +1 @@ +from .routes import router diff --git a/game/server/navmesh/models.py b/game/server/navmesh/models.py new file mode 100644 index 00000000..7bfe7199 --- /dev/null +++ b/game/server/navmesh/models.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from pydantic import BaseModel + +from game.server.leaflet import LeafletPoly + + +class NavMeshPolyJs(BaseModel): + poly: LeafletPoly + threatened: bool diff --git a/game/server/navmesh/routes.py b/game/server/navmesh/routes.py new file mode 100644 index 00000000..becba8da --- /dev/null +++ b/game/server/navmesh/routes.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, Depends + +from game import Game +from game.server import GameContext +from .models import NavMeshPolyJs +from ..leaflet import ShapelyUtil + +router: APIRouter = APIRouter(prefix="/navmesh") + + +@router.get("/", response_model=list[NavMeshPolyJs]) +def get(for_player: bool, game: Game = Depends(GameContext.get)) -> list[NavMeshPolyJs]: + mesh = game.coalition_for(for_player).nav_mesh + return [ + NavMeshPolyJs( + poly=ShapelyUtil.poly_to_leaflet(p.poly, game.theater), + threatened=p.threatened, + ) + for p in mesh.polys + ] diff --git a/game/sim/__init__.py b/game/sim/__init__.py index e403f332..fbe75102 100644 --- a/game/sim/__init__.py +++ b/game/sim/__init__.py @@ -1 +1,2 @@ +from .gameupdateevents import GameUpdateEvents from .missionsimulation import MissionSimulation diff --git a/game/sim/gameupdateevents.py b/game/sim/gameupdateevents.py index 02f49dbd..7c49182e 100644 --- a/game/sim/gameupdateevents.py +++ b/game/sim/gameupdateevents.py @@ -15,6 +15,7 @@ class GameUpdateEvents: self.new_combats: list[FrozenCombat] = [] self.updated_combats: list[FrozenCombat] = [] self.updated_flights: list[tuple[Flight, Point]] = [] + self.navmesh_updates: set[bool] = set() @property def empty(self) -> bool: @@ -24,6 +25,7 @@ class GameUpdateEvents: self.new_combats, self.updated_combats, self.updated_flights, + self.navmesh_updates, ] ) @@ -38,3 +40,6 @@ class GameUpdateEvents: def update_flight(self, flight: Flight, new_position: Point) -> None: self.updated_flights.append((flight, new_position)) + + def update_navmesh(self, player: bool) -> None: + self.navmesh_updates.add(player) diff --git a/qt_ui/widgets/map/model/mapmodel.py b/qt_ui/widgets/map/model/mapmodel.py index bf936c70..d173d590 100644 --- a/qt_ui/widgets/map/model/mapmodel.py +++ b/qt_ui/widgets/map/model/mapmodel.py @@ -21,7 +21,6 @@ from .flightjs import FlightJs from .frontlinejs import FrontLineJs from .groundobjectjs import GroundObjectJs from .mapzonesjs import MapZonesJs -from .navmeshjs import NavMeshJs from .supplyroutejs import SupplyRouteJs from .threatzonecontainerjs import ThreatZoneContainerJs from .threatzonesjs import ThreatZonesJs @@ -55,7 +54,6 @@ class MapModel(QObject): flightsChanged = Signal() frontLinesChanged = Signal() threatZonesChanged = Signal() - navmeshesChanged = Signal() mapZonesChanged = Signal() unculledZonesChanged = Signal() selectedFlightChanged = Signal(str) @@ -72,7 +70,6 @@ class MapModel(QObject): self._threat_zones = ThreatZoneContainerJs( ThreatZonesJs.empty(), ThreatZonesJs.empty() ) - self._navmeshes = NavMeshJs([], []) self._map_zones = MapZonesJs([], [], []) self._unculled_zones = [] self._selected_flight_index: Optional[Tuple[int, int]] = None @@ -102,7 +99,6 @@ class MapModel(QObject): self._threat_zones = ThreatZoneContainerJs( ThreatZonesJs.empty(), ThreatZonesJs.empty() ) - self._navmeshes = NavMeshJs([], []) self._map_zones = MapZonesJs([], [], []) self._unculled_zones = [] self.cleared.emit() @@ -168,7 +164,6 @@ class MapModel(QObject): self.reset_atos() self.reset_front_lines() self.reset_threat_zones() - self.reset_navmeshes() self.reset_map_zones() self.reset_unculled_zones() @@ -302,20 +297,12 @@ class MapModel(QObject): def threatZones(self) -> ThreatZoneContainerJs: return self._threat_zones - def reset_navmeshes(self) -> None: - self._navmeshes = NavMeshJs.from_game(self.game) - self.navmeshesChanged.emit() - - @Property(NavMeshJs, notify=navmeshesChanged) - def navmeshes(self) -> NavMeshJs: - return self._navmeshes - def reset_map_zones(self) -> None: self._map_zones = MapZonesJs.from_game(self.game) self.mapZonesChanged.emit() @Property(MapZonesJs, notify=mapZonesChanged) - def mapZones(self) -> NavMeshJs: + def mapZones(self) -> MapZonesJs: return self._map_zones def on_package_change(self) -> None: diff --git a/qt_ui/widgets/map/model/navmeshjs.py b/qt_ui/widgets/map/model/navmeshjs.py deleted file mode 100644 index a2b11d5a..00000000 --- a/qt_ui/widgets/map/model/navmeshjs.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -from PySide2.QtCore import Property, QObject, Signal - -from game import Game -from game.navmesh import NavMesh -from game.server.leaflet import LeafletPoly -from game.theater import ConflictTheater -from .navmeshpolyjs import NavMeshPolyJs - - -class NavMeshJs(QObject): - blueChanged = Signal() - redChanged = Signal() - - def __init__(self, blue: list[NavMeshPolyJs], red: list[NavMeshPolyJs]) -> None: - super().__init__() - self._blue = blue - self._red = red - # TODO: Boundary markers. - # TODO: Numbering. - # TODO: Localization debugging. - - @Property(list, notify=blueChanged) - def blue(self) -> list[LeafletPoly]: - return self._blue - - @Property(list, notify=redChanged) - def red(self) -> list[LeafletPoly]: - return self._red - - @staticmethod - def to_polys(navmesh: NavMesh, theater: ConflictTheater) -> list[NavMeshPolyJs]: - polys = [] - for poly in navmesh.polys: - polys.append(NavMeshPolyJs.from_navmesh(poly, theater)) - return polys - - @classmethod - def from_game(cls, game: Game) -> NavMeshJs: - return NavMeshJs( - cls.to_polys(game.blue.nav_mesh, game.theater), - cls.to_polys(game.red.nav_mesh, game.theater), - ) diff --git a/qt_ui/widgets/map/model/navmeshpolyjs.py b/qt_ui/widgets/map/model/navmeshpolyjs.py deleted file mode 100644 index 52d35992..00000000 --- a/qt_ui/widgets/map/model/navmeshpolyjs.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -from PySide2.QtCore import Property, QObject, Signal - -from game.navmesh import NavMeshPoly -from game.server.leaflet import LeafletPoly, ShapelyUtil -from game.theater import ConflictTheater - - -class NavMeshPolyJs(QObject): - polyChanged = Signal() - threatenedChanged = Signal() - - def __init__(self, poly: LeafletPoly, threatened: bool) -> None: - super().__init__() - self._poly = poly - self._threatened = threatened - - @Property(list, notify=polyChanged) - def poly(self) -> LeafletPoly: - return self._poly - - @Property(bool, notify=threatenedChanged) - def threatened(self) -> bool: - return self._threatened - - @classmethod - def from_navmesh(cls, poly: NavMeshPoly, theater: ConflictTheater) -> NavMeshPolyJs: - return NavMeshPolyJs( - ShapelyUtil.poly_to_leaflet(poly.poly, theater), poly.threatened - ) diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js index 83136fbc..08a624a5 100644 --- a/resources/ui/map/map.js +++ b/resources/ui/map/map.js @@ -384,7 +384,6 @@ new QWebChannel(qt.webChannelTransport, function (channel) { game.frontLinesChanged.connect(drawFrontLines); game.flightsChanged.connect(drawAircraft); game.threatZonesChanged.connect(drawThreatZones); - game.navmeshesChanged.connect(drawNavmeshes); game.mapZonesChanged.connect(drawMapZones); game.unculledZonesChanged.connect(drawUnculledZones); game.selectedFlightChanged.connect(updateSelectedFlight); @@ -400,6 +399,9 @@ function handleStreamedEvents(events) { for (const combat of events.updated_combats) { redrawCombat(combat); } + for (const player of events.navmesh_updates) { + drawNavmesh(player); + } } function recenterMap(center) { @@ -1094,26 +1096,27 @@ function drawThreatZones() { ); } -function drawNavmesh(zones, layer) { - for (const zone of zones) { - L.polyline(zone.poly, { - color: "#000000", - weight: 1, - fillColor: zone.threatened ? "#ff0000" : "#00ff00", - fill: true, - fillOpacity: 0.1, - noClip: true, - interactive: false, - }).addTo(layer); - } +function drawNavmesh(player) { + const layer = player ? blueNavmesh : redNavmesh; + layer.clearLayers(); + getJson(`/navmesh?for_player=${player}`).then((zones) => { + for (const zone of zones) { + L.polyline(zone.poly, { + color: "#000000", + weight: 1, + fillColor: zone.threatened ? "#ff0000" : "#00ff00", + fill: true, + fillOpacity: 0.1, + noClip: true, + interactive: false, + }).addTo(layer); + } + }); } function drawNavmeshes() { - blueNavmesh.clearLayers(); - redNavmesh.clearLayers(); - - drawNavmesh(game.navmeshes.blue, blueNavmesh); - drawNavmesh(game.navmeshes.red, redNavmesh); + drawNavmesh(true); + drawNavmesh(false); } function drawMapZones() {