Move NavMesh out of MapModel.

This commit is contained in:
Dan Albert 2022-02-22 18:47:51 -08:00
parent 1a9930b93a
commit 0e6a303c17
15 changed files with 108 additions and 127 deletions

View File

@ -21,10 +21,11 @@ from game.threatzones import ThreatZones
from game.transfers import PendingTransfers from game.transfers import PendingTransfers
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from .campaignloader import CampaignAirWingConfig
from game.campaignloader import CampaignAirWingConfig from .data.doctrine import Doctrine
from game.data.doctrine import Doctrine from .factions.faction import Faction
from game.factions.faction import Faction from .game import Game
from .sim import GameUpdateEvents
class Coalition: class Coalition:
@ -121,10 +122,11 @@ class Coalition:
def compute_threat_zones(self) -> None: def compute_threat_zones(self) -> None:
self._threat_zone = ThreatZones.for_faction(self.game, self.player) 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._navmesh = NavMesh.from_threat_zones(
self.opponent.threat_zone, self.game.theater self.opponent.threat_zone, self.game.theater
) )
events.update_navmesh(self.player)
def update_transit_network(self) -> None: def update_transit_network(self) -> None:
self.transit_network = TransitNetworkBuilder( self.transit_network = TransitNetworkBuilder(

View File

@ -15,11 +15,11 @@ from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from faker import Faker from faker import Faker
from game.ato.closestairfields import ObjectiveDistanceCache
from game.ground_forces.ai_ground_planner import GroundPlanner from game.ground_forces.ai_ground_planner import GroundPlanner
from game.models.game_stats import GameStats from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.utils import Distance from game.utils import Distance
from game.ato.closestairfields import ObjectiveDistanceCache
from . import naming, persistency from . import naming, persistency
from .ato.flighttype import FlightType from .ato.flighttype import FlightType
from .campaignloader import CampaignAirWingConfig from .campaignloader import CampaignAirWingConfig
@ -40,10 +40,11 @@ from .weather import Conditions, TimeOfDay
if TYPE_CHECKING: if TYPE_CHECKING:
from .ato.airtaaskingorder import AirTaskingOrder from .ato.airtaaskingorder import AirTaskingOrder
from .factions.faction import Faction
from .navmesh import NavMesh from .navmesh import NavMesh
from .sim import GameUpdateEvents
from .squadrons import AirWing from .squadrons import AirWing
from .threatzones import ThreatZones from .threatzones import ThreatZones
from .factions.faction import Faction
COMMISION_UNIT_VARIETY = 4 COMMISION_UNIT_VARIETY = 4
COMMISION_LIMITS_SCALE = 1.5 COMMISION_LIMITS_SCALE = 1.5
@ -203,6 +204,8 @@ class Game:
self.coalition_for(player).adjust_budget(amount) self.coalition_for(player).adjust_budget(amount)
def on_load(self, game_still_initializing: bool = False) -> None: def on_load(self, game_still_initializing: bool = False) -> None:
from .sim import GameUpdateEvents
if not hasattr(self, "name_generator"): if not hasattr(self, "name_generator"):
self.name_generator = naming.namegen self.name_generator = naming.namegen
# Hack: Replace the global name generator state with the state from the save # Hack: Replace the global name generator state with the state from the save
@ -215,7 +218,9 @@ class Game:
ObjectiveDistanceCache.set_theater(self.theater) ObjectiveDistanceCache.set_theater(self.theater)
self.compute_unculled_zones() self.compute_unculled_zones()
if not game_still_initializing: 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: def finish_turn(self, skipped: bool = False) -> None:
"""Finalizes the current turn and advances to the next turn. """Finalizes the current turn and advances to the next turn.
@ -266,9 +271,13 @@ class Game:
def begin_turn_0(self) -> None: def begin_turn_0(self) -> None:
"""Initialization for the first turn of the game.""" """Initialization for the first turn of the game."""
from .sim import GameUpdateEvents
self.blue.preinit_turn_0() self.blue.preinit_turn_0()
self.red.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: def pass_turn(self, no_action: bool = False) -> None:
"""Ends the current turn and initializes the new turn. """Ends the current turn and initializes the new turn.
@ -278,11 +287,18 @@ class Game:
Args: Args:
no_action: True if the turn was skipped. no_action: True if the turn was skipped.
""" """
from .server import EventStream
from .sim import GameUpdateEvents
logging.info("Pass turn") logging.info("Pass turn")
with logged_duration("Turn finalization"): with logged_duration("Turn finalization"):
self.finish_turn(no_action) self.finish_turn(no_action)
events = GameUpdateEvents()
with logged_duration("Turn initialization"): with logged_duration("Turn initialization"):
self.initialize_turn() self.initialize_turn(events)
EventStream.put_nowait(events)
# Autosave progress # Autosave progress
persistency.autosave(self) persistency.autosave(self)
@ -307,7 +323,9 @@ class Game:
self.blue.bullseye = Bullseye(enemy_cp.position) self.blue.bullseye = Bullseye(enemy_cp.position)
self.red.bullseye = Bullseye(player_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. """Performs turn initialization for the specified players.
Turn initialization performs all of the beginning-of-turn actions. *End-of-turn* 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. impactful but also likely to be early, so they also cause a blue replan.
Args: Args:
events: Game update event container for turn initialization.
for_red: True if opfor should be re-initialized. for_red: True if opfor should be re-initialized.
for_blue: True if the player coalition 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 # Plan flights & combat for next turn
with logged_duration("Threat zone computation"): with logged_duration("Threat zone computation"):
self.compute_threat_zones() self.compute_threat_zones(events)
# Plan Coalition specific turn # Plan Coalition specific turn
if for_blue: if for_blue:
@ -401,11 +420,11 @@ class Game:
def compute_transit_network_for(self, player: bool) -> TransitNetwork: def compute_transit_network_for(self, player: bool) -> TransitNetwork:
return TransitNetworkBuilder(self.theater, player).build() 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.blue.compute_threat_zones()
self.red.compute_threat_zones() self.red.compute_threat_zones()
self.blue.compute_nav_meshes() self.blue.compute_nav_meshes(events)
self.red.compute_nav_meshes() self.red.compute_nav_meshes(events)
def threat_zone_for(self, player: bool) -> ThreatZones: def threat_zone_for(self, player: bool) -> ThreatZones:
return self.coalition_for(player).threat_zone return self.coalition_for(player).threat_zone

View File

@ -1,10 +1,11 @@
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI
from . import debuggeometries, eventstream, flights, waypoints from . import debuggeometries, eventstream, flights, navmesh, waypoints
from .security import ApiKeyManager from .security import ApiKeyManager
app = FastAPI(dependencies=[Depends(ApiKeyManager.verify)]) app = FastAPI(dependencies=[Depends(ApiKeyManager.verify)])
app.include_router(debuggeometries.router) app.include_router(debuggeometries.router)
app.include_router(eventstream.router) app.include_router(eventstream.router)
app.include_router(flights.router) app.include_router(flights.router)
app.include_router(navmesh.router)
app.include_router(waypoints.router) app.include_router(waypoints.router)

View File

@ -1,5 +1,10 @@
from game import Game from __future__ import annotations
from qt_ui.models import GameModel
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from game import Game
from qt_ui.models import GameModel
class GameContext: class GameContext:

View File

@ -1,6 +1,6 @@
from asyncio import Queue from asyncio import Queue
from game.sim.gameupdateevents import GameUpdateEvents from game.sim import GameUpdateEvents
class EventStream: class EventStream:

View File

@ -10,13 +10,14 @@ from game.server.leaflet import LeafletLatLon
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.sim.gameupdateevents import GameUpdateEvents from game.sim import GameUpdateEvents
class GameUpdateEventsJs(BaseModel): class GameUpdateEventsJs(BaseModel):
updated_flights: dict[UUID, LeafletLatLon] updated_flights: dict[UUID, LeafletLatLon]
new_combats: list[FrozenCombatJs] = [] new_combats: list[FrozenCombatJs] = []
updated_combats: list[FrozenCombatJs] = [] updated_combats: list[FrozenCombatJs] = []
navmesh_updates: set[bool] = set()
@classmethod @classmethod
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs: def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
@ -31,4 +32,5 @@ class GameUpdateEventsJs(BaseModel):
FrozenCombatJs.for_combat(c, game.theater) FrozenCombatJs.for_combat(c, game.theater)
for c in events.updated_combats for c in events.updated_combats
], ],
navmesh_updates=events.navmesh_updates,
) )

View File

@ -0,0 +1 @@
from .routes import router

View File

@ -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

View File

@ -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
]

View File

@ -1 +1,2 @@
from .gameupdateevents import GameUpdateEvents
from .missionsimulation import MissionSimulation from .missionsimulation import MissionSimulation

View File

@ -15,6 +15,7 @@ class GameUpdateEvents:
self.new_combats: list[FrozenCombat] = [] self.new_combats: list[FrozenCombat] = []
self.updated_combats: list[FrozenCombat] = [] self.updated_combats: list[FrozenCombat] = []
self.updated_flights: list[tuple[Flight, Point]] = [] self.updated_flights: list[tuple[Flight, Point]] = []
self.navmesh_updates: set[bool] = set()
@property @property
def empty(self) -> bool: def empty(self) -> bool:
@ -24,6 +25,7 @@ class GameUpdateEvents:
self.new_combats, self.new_combats,
self.updated_combats, self.updated_combats,
self.updated_flights, self.updated_flights,
self.navmesh_updates,
] ]
) )
@ -38,3 +40,6 @@ class GameUpdateEvents:
def update_flight(self, flight: Flight, new_position: Point) -> None: def update_flight(self, flight: Flight, new_position: Point) -> None:
self.updated_flights.append((flight, new_position)) self.updated_flights.append((flight, new_position))
def update_navmesh(self, player: bool) -> None:
self.navmesh_updates.add(player)

View File

@ -21,7 +21,6 @@ from .flightjs import FlightJs
from .frontlinejs import FrontLineJs from .frontlinejs import FrontLineJs
from .groundobjectjs import GroundObjectJs from .groundobjectjs import GroundObjectJs
from .mapzonesjs import MapZonesJs from .mapzonesjs import MapZonesJs
from .navmeshjs import NavMeshJs
from .supplyroutejs import SupplyRouteJs from .supplyroutejs import SupplyRouteJs
from .threatzonecontainerjs import ThreatZoneContainerJs from .threatzonecontainerjs import ThreatZoneContainerJs
from .threatzonesjs import ThreatZonesJs from .threatzonesjs import ThreatZonesJs
@ -55,7 +54,6 @@ class MapModel(QObject):
flightsChanged = Signal() flightsChanged = Signal()
frontLinesChanged = Signal() frontLinesChanged = Signal()
threatZonesChanged = Signal() threatZonesChanged = Signal()
navmeshesChanged = Signal()
mapZonesChanged = Signal() mapZonesChanged = Signal()
unculledZonesChanged = Signal() unculledZonesChanged = Signal()
selectedFlightChanged = Signal(str) selectedFlightChanged = Signal(str)
@ -72,7 +70,6 @@ class MapModel(QObject):
self._threat_zones = ThreatZoneContainerJs( self._threat_zones = ThreatZoneContainerJs(
ThreatZonesJs.empty(), ThreatZonesJs.empty() ThreatZonesJs.empty(), ThreatZonesJs.empty()
) )
self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], []) self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = [] self._unculled_zones = []
self._selected_flight_index: Optional[Tuple[int, int]] = None self._selected_flight_index: Optional[Tuple[int, int]] = None
@ -102,7 +99,6 @@ class MapModel(QObject):
self._threat_zones = ThreatZoneContainerJs( self._threat_zones = ThreatZoneContainerJs(
ThreatZonesJs.empty(), ThreatZonesJs.empty() ThreatZonesJs.empty(), ThreatZonesJs.empty()
) )
self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], []) self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = [] self._unculled_zones = []
self.cleared.emit() self.cleared.emit()
@ -168,7 +164,6 @@ class MapModel(QObject):
self.reset_atos() self.reset_atos()
self.reset_front_lines() self.reset_front_lines()
self.reset_threat_zones() self.reset_threat_zones()
self.reset_navmeshes()
self.reset_map_zones() self.reset_map_zones()
self.reset_unculled_zones() self.reset_unculled_zones()
@ -302,20 +297,12 @@ class MapModel(QObject):
def threatZones(self) -> ThreatZoneContainerJs: def threatZones(self) -> ThreatZoneContainerJs:
return self._threat_zones 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: def reset_map_zones(self) -> None:
self._map_zones = MapZonesJs.from_game(self.game) self._map_zones = MapZonesJs.from_game(self.game)
self.mapZonesChanged.emit() self.mapZonesChanged.emit()
@Property(MapZonesJs, notify=mapZonesChanged) @Property(MapZonesJs, notify=mapZonesChanged)
def mapZones(self) -> NavMeshJs: def mapZones(self) -> MapZonesJs:
return self._map_zones return self._map_zones
def on_package_change(self) -> None: def on_package_change(self) -> None:

View File

@ -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),
)

View File

@ -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
)

View File

@ -384,7 +384,6 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.frontLinesChanged.connect(drawFrontLines); game.frontLinesChanged.connect(drawFrontLines);
game.flightsChanged.connect(drawAircraft); game.flightsChanged.connect(drawAircraft);
game.threatZonesChanged.connect(drawThreatZones); game.threatZonesChanged.connect(drawThreatZones);
game.navmeshesChanged.connect(drawNavmeshes);
game.mapZonesChanged.connect(drawMapZones); game.mapZonesChanged.connect(drawMapZones);
game.unculledZonesChanged.connect(drawUnculledZones); game.unculledZonesChanged.connect(drawUnculledZones);
game.selectedFlightChanged.connect(updateSelectedFlight); game.selectedFlightChanged.connect(updateSelectedFlight);
@ -400,6 +399,9 @@ function handleStreamedEvents(events) {
for (const combat of events.updated_combats) { for (const combat of events.updated_combats) {
redrawCombat(combat); redrawCombat(combat);
} }
for (const player of events.navmesh_updates) {
drawNavmesh(player);
}
} }
function recenterMap(center) { function recenterMap(center) {
@ -1094,26 +1096,27 @@ function drawThreatZones() {
); );
} }
function drawNavmesh(zones, layer) { function drawNavmesh(player) {
for (const zone of zones) { const layer = player ? blueNavmesh : redNavmesh;
L.polyline(zone.poly, { layer.clearLayers();
color: "#000000", getJson(`/navmesh?for_player=${player}`).then((zones) => {
weight: 1, for (const zone of zones) {
fillColor: zone.threatened ? "#ff0000" : "#00ff00", L.polyline(zone.poly, {
fill: true, color: "#000000",
fillOpacity: 0.1, weight: 1,
noClip: true, fillColor: zone.threatened ? "#ff0000" : "#00ff00",
interactive: false, fill: true,
}).addTo(layer); fillOpacity: 0.1,
} noClip: true,
interactive: false,
}).addTo(layer);
}
});
} }
function drawNavmeshes() { function drawNavmeshes() {
blueNavmesh.clearLayers(); drawNavmesh(true);
redNavmesh.clearLayers(); drawNavmesh(false);
drawNavmesh(game.navmeshes.blue, blueNavmesh);
drawNavmesh(game.navmeshes.red, redNavmesh);
} }
function drawMapZones() { function drawMapZones() {