Handle map reset when the game is loaded/unloaded.

https://github.com/dcs-liberation/dcs_liberation/issues/2039

Partial fix for
https://github.com/dcs-liberation/dcs_liberation/issues/2045 (now works
in the new map, old one not fixed yet).
This commit is contained in:
Dan Albert
2022-03-05 18:02:46 -08:00
parent 995e28cb32
commit 73fcfcec7b
37 changed files with 403 additions and 178 deletions

View File

@@ -7,6 +7,7 @@ from . import (
eventstream,
flights,
frontlines,
game,
mapzones,
navmesh,
qt,
@@ -27,6 +28,7 @@ app.include_router(debuggeometries.router)
app.include_router(eventstream.router)
app.include_router(flights.router)
app.include_router(frontlines.router)
app.include_router(game.router)
app.include_router(mapzones.router)
app.include_router(navmesh.router)
app.include_router(qt.router)

View File

@@ -1,9 +1,14 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pydantic import BaseModel
from game.server.leaflet import LeafletPoint
from game.theater import ControlPoint
if TYPE_CHECKING:
from game import Game
from game.theater import ControlPoint
class ControlPointJs(BaseModel):
@@ -29,3 +34,9 @@ class ControlPointJs(BaseModel):
destination=destination,
sidc=str(control_point.sidc()),
)
@staticmethod
def all_in_game(game: Game) -> list[ControlPointJs]:
return [
ControlPointJs.for_control_point(cp) for cp in game.theater.controlpoints
]

View File

@@ -13,16 +13,15 @@ router: APIRouter = APIRouter(prefix="/control-points")
@router.get("/")
def list_control_points(game: Game = Depends(GameContext.get)) -> list[ControlPointJs]:
control_points = []
for control_point in game.theater.controlpoints:
control_points.append(ControlPointJs.for_control_point(control_point))
return control_points
def list_control_points(
game: Game = Depends(GameContext.require),
) -> list[ControlPointJs]:
return ControlPointJs.all_in_game(game)
@router.get("/{cp_id}")
def get_control_point(
cp_id: int, game: Game = Depends(GameContext.get)
cp_id: int, game: Game = Depends(GameContext.require)
) -> ControlPointJs:
cp = game.theater.find_control_point_by_id(cp_id)
if cp is None:
@@ -35,7 +34,7 @@ def get_control_point(
@router.get("/{cp_id}/destination-in-range")
def destination_in_range(
cp_id: int, lat: float, lng: float, game: Game = Depends(GameContext.get)
cp_id: int, lat: float, lng: float, game: Game = Depends(GameContext.require)
) -> bool:
cp = game.theater.find_control_point_by_id(cp_id)
if cp is None:
@@ -50,7 +49,7 @@ def destination_in_range(
@router.put("/{cp_id}/destination")
def set_destination(
cp_id: int, destination: LeafletPoint, game: Game = Depends(GameContext.get)
cp_id: int, destination: LeafletPoint, game: Game = Depends(GameContext.require)
) -> None:
cp = game.theater.find_control_point_by_id(cp_id)
if cp is None:
@@ -79,7 +78,7 @@ def set_destination(
@router.put("/{cp_id}/cancel-travel")
def cancel_travel(cp_id: int, game: Game = Depends(GameContext.get)) -> None:
def cancel_travel(cp_id: int, game: Game = Depends(GameContext.require)) -> None:
cp = game.theater.find_control_point_by_id(cp_id)
if cp is None:
raise HTTPException(

View File

@@ -10,15 +10,19 @@ router: APIRouter = APIRouter(prefix="/debug/waypoint-geometries")
@router.get("/hold/{flight_id}")
def hold_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> HoldZonesJs:
def hold_zones(
flight_id: UUID, game: Game = Depends(GameContext.require)
) -> HoldZonesJs:
return HoldZonesJs.for_flight(game.db.flights.get(flight_id), game)
@router.get("/ip/{flight_id}")
def ip_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> IpZonesJs:
def ip_zones(flight_id: UUID, game: Game = Depends(GameContext.require)) -> IpZonesJs:
return IpZonesJs.for_flight(game.db.flights.get(flight_id), game)
@router.get("/join/{flight_id}")
def join_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> JoinZonesJs:
def join_zones(
flight_id: UUID, game: Game = Depends(GameContext.require)
) -> JoinZonesJs:
return JoinZonesJs.for_flight(game.db.flights.get(flight_id), game)

View File

@@ -17,7 +17,11 @@ class GameContext:
cls._game_model = game_model
@classmethod
def get(cls) -> Game:
def get(cls) -> Game | None:
return cls._game_model.game
@classmethod
def require(cls) -> Game:
if cls._game_model.game is None:
raise RuntimeError("GameContext has no Game set")
return cls._game_model.game

View File

@@ -33,21 +33,38 @@ class GameUpdateEventsJs(BaseModel):
deleted_front_lines: set[UUID]
updated_tgos: set[UUID]
updated_control_points: set[int]
reset_on_map_center: LeafletLatLon | None
game_unloaded: bool
@classmethod
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
def from_events(
cls, events: GameUpdateEvents, game: Game | None
) -> GameUpdateEventsJs:
# We still need to be able to send update events when there is no game loaded
# because we need to send the unload event.
new_combats = []
updated_combats = []
if game is not None:
new_combats = [
FrozenCombatJs.for_combat(c, game.theater) for c in events.new_combats
]
updated_combats = [
FrozenCombatJs.for_combat(c, game.theater)
for c in events.updated_combats
]
recenter_map = None
if events.reset_on_map_center is not None:
recenter_map = events.reset_on_map_center.as_list()
return GameUpdateEventsJs(
updated_flight_positions={
f[0].id: f[1].latlng().as_list()
for f in events.updated_flight_positions
},
new_combats=[
FrozenCombatJs.for_combat(c, game.theater) for c in events.new_combats
],
updated_combats=[
FrozenCombatJs.for_combat(c, game.theater)
for c in events.updated_combats
],
new_combats=new_combats,
updated_combats=updated_combats,
ended_combats=[c.id for c in events.ended_combats],
navmesh_updates=events.navmesh_updates,
unculled_zones_updated=events.unculled_zones_updated,
@@ -66,4 +83,6 @@ class GameUpdateEventsJs(BaseModel):
deleted_front_lines=events.deleted_front_lines,
updated_tgos=events.updated_tgos,
updated_control_points=events.updated_control_points,
reset_on_map_center=recenter_map,
game_unloaded=events.game_unloaded,
)

View File

@@ -1,15 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from uuid import UUID
from pydantic import BaseModel
from game.ato import Flight
from game.ato.flightstate import InFlight
from game.server.leaflet import LeafletPoint
from game.server.waypoints.models import FlightWaypointJs
from game.server.waypoints.routes import waypoints_for_flight
if TYPE_CHECKING:
from game import Game
from game.ato import Flight
class FlightJs(BaseModel):
id: UUID
@@ -37,3 +41,12 @@ class FlightJs(BaseModel):
sidc=str(flight.sidc()),
waypoints=waypoints,
)
@staticmethod
def all_in_game(game: Game, with_waypoints: bool) -> list[FlightJs]:
flights = []
for coalition in game.coalitions:
for package in coalition.ato.packages:
for flight in package.flights:
flights.append(FlightJs.for_flight(flight, with_waypoints))
return flights

View File

@@ -14,19 +14,16 @@ router: APIRouter = APIRouter(prefix="/flights")
@router.get("/")
def list_flights(
with_waypoints: bool = False, game: Game = Depends(GameContext.get)
with_waypoints: bool = False, game: Game = Depends(GameContext.require)
) -> list[FlightJs]:
flights = []
for coalition in game.coalitions:
for package in coalition.ato.packages:
for flight in package.flights:
flights.append(FlightJs.for_flight(flight, with_waypoints))
return flights
return FlightJs.all_in_game(game, with_waypoints)
@router.get("/{flight_id}")
def get_flight(
flight_id: UUID, with_waypoints: bool = False, game: Game = Depends(GameContext.get)
flight_id: UUID,
with_waypoints: bool = False,
game: Game = Depends(GameContext.require),
) -> FlightJs:
flight = game.db.flights.get(flight_id)
return FlightJs.for_flight(flight, with_waypoints)
@@ -34,7 +31,7 @@ def get_flight(
@router.get("/{flight_id}/commit-boundary")
def commit_boundary(
flight_id: UUID, game: Game = Depends(GameContext.get)
flight_id: UUID, game: Game = Depends(GameContext.require)
) -> LeafletPoly:
flight = game.db.flights.get(flight_id)
if not isinstance(flight.flight_plan, PatrollingFlightPlan):

View File

@@ -1,13 +1,17 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from uuid import UUID
from pydantic import BaseModel
from game.server.leaflet import LeafletPoint
from game.theater import FrontLine
from game.utils import nautical_miles
if TYPE_CHECKING:
from game import Game
from game.theater import FrontLine
class FrontLineJs(BaseModel):
id: UUID
@@ -22,3 +26,7 @@ class FrontLineJs(BaseModel):
front_line.attack_heading.left.degrees, nautical_miles(2).meters
)
return FrontLineJs(id=front_line.id, extents=[a.latlng(), b.latlng()])
@staticmethod
def all_in_game(game: Game) -> list[FrontLineJs]:
return [FrontLineJs.for_front_line(f) for f in game.theater.conflicts()]

View File

@@ -10,12 +10,12 @@ router: APIRouter = APIRouter(prefix="/front-lines")
@router.get("/")
def list_front_lines(game: Game = Depends(GameContext.get)) -> list[FrontLineJs]:
return [FrontLineJs.for_front_line(f) for f in game.theater.conflicts()]
def list_front_lines(game: Game = Depends(GameContext.require)) -> list[FrontLineJs]:
return FrontLineJs.all_in_game(game)
@router.get("/{front_line_id}")
def get_front_line(
front_line_id: UUID, game: Game = Depends(GameContext.get)
front_line_id: UUID, game: Game = Depends(GameContext.require)
) -> FrontLineJs:
return FrontLineJs.for_front_line(game.db.front_lines.get(front_line_id))

View File

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

View File

@@ -0,0 +1,35 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pydantic import BaseModel
from game.server.controlpoints.models import ControlPointJs
from game.server.flights.models import FlightJs
from game.server.frontlines.models import FrontLineJs
from game.server.leaflet import LeafletPoint
from game.server.supplyroutes.models import SupplyRouteJs
from game.server.tgos.models import TgoJs
if TYPE_CHECKING:
from game import Game
class GameJs(BaseModel):
control_points: list[ControlPointJs]
tgos: list[TgoJs]
supply_routes: list[SupplyRouteJs]
front_lines: list[FrontLineJs]
flights: list[FlightJs]
map_center: LeafletPoint
@staticmethod
def from_game(game: Game) -> GameJs:
return GameJs(
control_points=ControlPointJs.all_in_game(game),
tgos=TgoJs.all_in_game(game),
supply_routes=SupplyRouteJs.all_in_game(game),
front_lines=FrontLineJs.all_in_game(game),
flights=FlightJs.all_in_game(game, with_waypoints=True),
map_center=game.theater.terrain.map_view_default.position.latlng(),
)

View File

@@ -0,0 +1,14 @@
from fastapi import APIRouter, Depends
from game import Game
from game.server import GameContext
from .models import GameJs
router: APIRouter = APIRouter(prefix="/game")
@router.get("/")
def game_state(game: Game | None = Depends(GameContext.get)) -> GameJs | None:
if game is None:
return None
return GameJs.from_game(game)

View File

@@ -9,7 +9,7 @@ router: APIRouter = APIRouter(prefix="/map-zones")
@router.get("/terrain")
def get_terrain(game: Game = Depends(GameContext.get)) -> MapZonesJs:
def get_terrain(game: Game = Depends(GameContext.require)) -> MapZonesJs:
zones = game.theater.landmap
if zones is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@@ -22,7 +22,9 @@ def get_terrain(game: Game = Depends(GameContext.get)) -> MapZonesJs:
@router.get("/unculled")
def get_unculled_zones(game: Game = Depends(GameContext.get)) -> list[UnculledZoneJs]:
def get_unculled_zones(
game: Game = Depends(GameContext.require),
) -> list[UnculledZoneJs]:
return [
UnculledZoneJs(
position=zone.latlng(), radius=game.settings.perf_culling_distance * 1000
@@ -32,7 +34,9 @@ def get_unculled_zones(game: Game = Depends(GameContext.get)) -> list[UnculledZo
@router.get("/threats")
def get_threat_zones(game: Game = Depends(GameContext.get)) -> ThreatZoneContainerJs:
def get_threat_zones(
game: Game = Depends(GameContext.require),
) -> ThreatZoneContainerJs:
return ThreatZoneContainerJs(
blue=ThreatZonesJs.from_zones(game.threat_zone_for(player=True), game.theater),
red=ThreatZonesJs.from_zones(game.threat_zone_for(player=False), game.theater),

View File

@@ -9,7 +9,9 @@ router: APIRouter = APIRouter(prefix="/navmesh")
@router.get("/", response_model=list[NavMeshPolyJs])
def get(for_player: bool, game: Game = Depends(GameContext.get)) -> list[NavMeshPolyJs]:
def get(
for_player: bool, game: Game = Depends(GameContext.require)
) -> list[NavMeshPolyJs]:
mesh = game.coalition_for(for_player).nav_mesh
return [
NavMeshPolyJs(

View File

@@ -11,7 +11,7 @@ router: APIRouter = APIRouter(prefix="/qt")
@router.post("/create-package/front-line/{front_line_id}")
def new_front_line_package(
front_line_id: UUID,
game: Game = Depends(GameContext.get),
game: Game = Depends(GameContext.require),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
qt.create_new_package(game.db.front_lines.get(front_line_id))
@@ -20,7 +20,7 @@ def new_front_line_package(
@router.post("/create-package/tgo/{tgo_id}")
def new_tgo_package(
tgo_id: UUID,
game: Game = Depends(GameContext.get),
game: Game = Depends(GameContext.require),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
qt.create_new_package(game.db.tgos.get(tgo_id))
@@ -29,7 +29,7 @@ def new_tgo_package(
@router.post("/info/tgo/{tgo_id}")
def show_tgo_info(
tgo_id: UUID,
game: Game = Depends(GameContext.get),
game: Game = Depends(GameContext.require),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
qt.show_tgo_info(game.db.tgos.get(tgo_id))
@@ -38,7 +38,7 @@ def show_tgo_info(
@router.post("/create-package/control-point/{cp_id}")
def new_cp_package(
cp_id: int,
game: Game = Depends(GameContext.get),
game: Game = Depends(GameContext.require),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
cp = game.theater.find_control_point_by_id(cp_id)
@@ -53,7 +53,7 @@ def new_cp_package(
@router.post("/info/control-point/{cp_id}")
def show_control_point_info(
cp_id: int,
game: Game = Depends(GameContext.get),
game: Game = Depends(GameContext.require),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
cp = game.theater.find_control_point_by_id(cp_id)

View File

@@ -81,3 +81,29 @@ class SupplyRouteJs(BaseModel):
sea
),
)
@staticmethod
def all_in_game(game: Game) -> list[SupplyRouteJs]:
seen = set()
routes = []
for control_point in game.theater.controlpoints:
seen.add(control_point)
for destination, route in control_point.convoy_routes.items():
if destination in seen:
continue
routes.append(
SupplyRouteJs.for_link(
game, control_point, destination, list(route), sea=False
)
)
for destination, route in control_point.shipping_lanes.items():
if destination in seen:
continue
if not destination.is_friendly_to(control_point):
continue
routes.append(
SupplyRouteJs.for_link(
game, control_point, destination, list(route), sea=True
)
)
return routes

View File

@@ -8,27 +8,7 @@ router: APIRouter = APIRouter(prefix="/supply-routes")
@router.get("/")
def list_supply_routes(game: Game = Depends(GameContext.get)) -> list[SupplyRouteJs]:
seen = set()
routes = []
for control_point in game.theater.controlpoints:
seen.add(control_point)
for destination, route in control_point.convoy_routes.items():
if destination in seen:
continue
routes.append(
SupplyRouteJs.for_link(
game, control_point, destination, list(route), sea=False
)
)
for destination, route in control_point.shipping_lanes.items():
if destination in seen:
continue
if not destination.is_friendly_to(control_point):
continue
routes.append(
SupplyRouteJs.for_link(
game, control_point, destination, list(route), sea=True
)
)
return routes
def list_supply_routes(
game: Game = Depends(GameContext.require),
) -> list[SupplyRouteJs]:
return SupplyRouteJs.all_in_game(game)

View File

@@ -1,11 +1,15 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from uuid import UUID
from pydantic import BaseModel
from game.server.leaflet import LeafletPoint
from game.theater import TheaterGroundObject
if TYPE_CHECKING:
from game import Game
from game.theater import TheaterGroundObject
class TgoJs(BaseModel):
@@ -44,3 +48,12 @@ class TgoJs(BaseModel):
dead=tgo.is_dead,
sidc=str(tgo.sidc()),
)
@staticmethod
def all_in_game(game: Game) -> list[TgoJs]:
tgos = []
for control_point in game.theater.controlpoints:
for tgo in control_point.connected_objectives:
if not tgo.is_control_point:
tgos.append(TgoJs.for_tgo(tgo))
return tgos

View File

@@ -10,15 +10,10 @@ router: APIRouter = APIRouter(prefix="/tgos")
@router.get("/")
def list_tgos(game: Game = Depends(GameContext.get)) -> list[TgoJs]:
tgos = []
for control_point in game.theater.controlpoints:
for tgo in control_point.connected_objectives:
if not tgo.is_control_point:
tgos.append(TgoJs.for_tgo(tgo))
return tgos
def list_tgos(game: Game = Depends(GameContext.require)) -> list[TgoJs]:
return TgoJs.all_in_game(game)
@router.get("/{tgo_id}")
def get_tgo(tgo_id: UUID, game: Game = Depends(GameContext.get)) -> TgoJs:
def get_tgo(tgo_id: UUID, game: Game = Depends(GameContext.require)) -> TgoJs:
return TgoJs.for_tgo(game.db.tgos.get(tgo_id))

View File

@@ -36,7 +36,7 @@ def waypoints_for_flight(flight: Flight) -> list[FlightWaypointJs]:
@router.get("/{flight_id}", response_model=list[FlightWaypointJs])
def all_waypoints_for_flight(
flight_id: UUID, game: Game = Depends(GameContext.get)
flight_id: UUID, game: Game = Depends(GameContext.require)
) -> list[FlightWaypointJs]:
return waypoints_for_flight(game.db.flights.get(flight_id))
@@ -46,7 +46,7 @@ def set_position(
flight_id: UUID,
waypoint_idx: int,
position: LeafletPoint,
game: Game = Depends(GameContext.get),
game: Game = Depends(GameContext.require),
) -> None:
from game.server import EventStream