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

View File

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

View File

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

View File

@ -1,3 +1,8 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from game import Game
from qt_ui.models import GameModel

View File

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

View File

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

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

View File

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

View File

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

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.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,7 +1096,10 @@ function drawThreatZones() {
);
}
function drawNavmesh(zones, 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",
@ -1106,14 +1111,12 @@ function drawNavmesh(zones, layer) {
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() {