Add FastAPI interface between the game and map.

A possible explanation for the infrequent CTDs we've been seeing since
adding fast forward is that QWebChannel doesn't keep a reference to the
python objects that it passes to js, so if the object is GC'd before the
front end is done with it, it crashes.

We don't really like QWebChannel anyway, so this begins replacing that
with FastAPI.
This commit is contained in:
Dan Albert 2022-02-13 14:22:05 -08:00
parent 1df31b2496
commit 350f08be2f
33 changed files with 419 additions and 330 deletions

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import uuid
from datetime import datetime, timedelta
from typing import Any, List, Optional, TYPE_CHECKING
@ -36,6 +37,7 @@ class Flight:
cargo: Optional[TransferOrder] = None,
roster: Optional[FlightRoster] = None,
) -> None:
self.id = uuid.uuid4()
self.package = package
self.country = country
self.squadron = squadron
@ -91,6 +93,8 @@ class Flight:
state["state"] = Uninitialized(self, state["squadron"].settings)
if "props" not in state:
state["props"] = {}
if "id" not in state:
state["id"] = uuid.uuid4()
self.__dict__.update(state)
@property

2
game/server/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .dependencies import GameContext
from .server import Server

6
game/server/app.py Normal file
View File

@ -0,0 +1,6 @@
from fastapi import FastAPI
from . import debuggeometries
app = FastAPI()
app.include_router(debuggeometries.router)

View File

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

View File

@ -0,0 +1,126 @@
from __future__ import annotations
from pydantic import BaseModel, Field
from game import Game
from game.ato import Flight
from game.flightplan import HoldZoneGeometry, IpZoneGeometry, JoinZoneGeometry
from ..leaflet import LeafletPoly, ShapelyUtil
class HoldZonesJs(BaseModel):
home_bubble: LeafletPoly = Field(alias="homeBubble")
target_bubble: LeafletPoly = Field(alias="targetBubble")
join_bubble: LeafletPoly = Field(alias="joinBubble")
excluded_zones: list[LeafletPoly] = Field(alias="excludedZones")
permissible_zones: list[LeafletPoly] = Field(alias="permissibleZones")
preferred_lines: list[LeafletPoly] = Field(alias="preferredLines")
@classmethod
def empty(cls) -> HoldZonesJs:
return HoldZonesJs(
homeBubble=[],
targetBubble=[],
joinBubble=[],
excludedZones=[],
permissibleZones=[],
preferredLines=[],
)
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> HoldZonesJs:
target = flight.package.target
home = flight.departure
if flight.package.waypoints is None:
return HoldZonesJs.empty()
ip = flight.package.waypoints.ingress
join = flight.package.waypoints.join
geometry = HoldZoneGeometry(
target.position, home.position, ip, join, game.blue, game.theater
)
return HoldZonesJs(
homeBubble=ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater),
targetBubble=ShapelyUtil.poly_to_leaflet(
geometry.target_bubble, game.theater
),
joinBubble=ShapelyUtil.poly_to_leaflet(geometry.join_bubble, game.theater),
excludedZones=ShapelyUtil.polys_to_leaflet(
geometry.excluded_zones, game.theater
),
permissibleZones=ShapelyUtil.polys_to_leaflet(
geometry.permissible_zones, game.theater
),
preferredLines=ShapelyUtil.lines_to_leaflet(
geometry.preferred_lines, game.theater
),
)
class IpZonesJs(BaseModel):
home_bubble: LeafletPoly = Field(alias="homeBubble")
ipBubble: LeafletPoly = Field(alias="ipBubble")
permissibleZone: LeafletPoly = Field(alias="permissibleZone")
safeZones: list[LeafletPoly] = Field(alias="safeZones")
@classmethod
def empty(cls) -> IpZonesJs:
return IpZonesJs(homeBubble=[], ipBubble=[], permissibleZone=[], safeZones=[])
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs:
target = flight.package.target
home = flight.departure
geometry = IpZoneGeometry(target.position, home.position, game.blue)
return IpZonesJs(
homeBubble=ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater),
ipBubble=ShapelyUtil.poly_to_leaflet(geometry.ip_bubble, game.theater),
permissibleZone=ShapelyUtil.poly_to_leaflet(
geometry.permissible_zone, game.theater
),
safeZones=ShapelyUtil.polys_to_leaflet(geometry.safe_zones, game.theater),
)
class JoinZonesJs(BaseModel):
home_bubble: LeafletPoly = Field(alias="homeBubble")
target_bubble: LeafletPoly = Field(alias="targetBubble")
ip_bubble: LeafletPoly = Field(alias="ipBubble")
excluded_zones: list[LeafletPoly] = Field(alias="excludedZones")
permissible_zones: list[LeafletPoly] = Field(alias="permissibleZones")
preferred_lines: list[LeafletPoly] = Field(alias="preferredLines")
@classmethod
def empty(cls) -> JoinZonesJs:
return JoinZonesJs(
homeBubble=[],
targetBubble=[],
ipBubble=[],
excludedZones=[],
permissibleZones=[],
preferredLines=[],
)
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> JoinZonesJs:
target = flight.package.target
home = flight.departure
if flight.package.waypoints is None:
return JoinZonesJs.empty()
ip = flight.package.waypoints.ingress
geometry = JoinZoneGeometry(target.position, home.position, ip, game.blue)
return JoinZonesJs(
homeBubble=ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater),
targetBubble=ShapelyUtil.poly_to_leaflet(
geometry.target_bubble, game.theater
),
ipBubble=ShapelyUtil.poly_to_leaflet(geometry.ip_bubble, game.theater),
excludedZones=ShapelyUtil.polys_to_leaflet(
geometry.excluded_zones, game.theater
),
permissibleZones=ShapelyUtil.polys_to_leaflet(
geometry.permissible_zones, game.theater
),
preferredLines=ShapelyUtil.lines_to_leaflet(
geometry.preferred_lines, game.theater
),
)

View File

@ -0,0 +1,38 @@
from uuid import UUID
from fastapi import APIRouter, Depends
from game import Game
from game.ato import Flight
from game.server import GameContext
from .models import HoldZonesJs, IpZonesJs, JoinZonesJs
router = APIRouter(prefix="/debug/waypoint-geometries")
# TODO: Maintain map of UUID -> Flight in Game.
def find_flight(game: Game, flight_id: UUID) -> Flight:
for coalition in game.coalitions:
for package in coalition.ato.packages:
for flight in package.flights:
if flight.id == flight_id:
return flight
raise KeyError(f"No flight found with ID {flight_id}")
@router.get("/hold/{flight_id}")
def hold_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> HoldZonesJs:
flight = find_flight(game, flight_id)
return HoldZonesJs.for_flight(flight, game)
@router.get("/ip/{flight_id}")
def ip_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> IpZonesJs:
flight = find_flight(game, flight_id)
return IpZonesJs.for_flight(flight, game)
@router.get("/join/{flight_id}")
def join_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> JoinZonesJs:
flight = find_flight(game, flight_id)
return JoinZonesJs.for_flight(flight, game)

View File

@ -0,0 +1,15 @@
from game import Game
class GameContext:
_game: Game | None
@classmethod
def set(cls, game: Game | None) -> None:
cls._game = game
@classmethod
def get(cls) -> Game:
if cls._game is None:
raise RuntimeError("GameContext has no Game set")
return cls._game

View File

@ -1,10 +1,14 @@
from __future__ import annotations
from typing import Union
from dcs import Point
from shapely.geometry import LineString, MultiLineString, MultiPolygon, Polygon
from game.theater import ConflictTheater
from .leaflet import LeafletLatLon, LeafletPoly
LeafletLatLon = list[float]
LeafletPoly = list[LeafletLatLon]
class ShapelyUtil:

35
game/server/server.py Normal file
View File

@ -0,0 +1,35 @@
import time
from collections.abc import Iterator
from contextlib import contextmanager
from threading import Thread
import uvicorn
from uvicorn import Config
from game.server.settings import ServerSettings
class Server(uvicorn.Server):
def __init__(self) -> None:
super().__init__(
Config(
"game.server.app:app",
host=ServerSettings.get().server_bind_address,
port=ServerSettings.get().server_port,
log_level="info",
)
)
@contextmanager
def run_in_thread(self) -> Iterator[None]:
# This relies on undocumented behavior, but it is what the developer recommends:
# https://github.com/encode/uvicorn/issues/742
thread = Thread(target=self.run)
thread.start()
try:
while not self.started:
time.sleep(1e-3)
yield
finally:
self.should_exit = True
thread.join()

28
game/server/settings.py Normal file
View File

@ -0,0 +1,28 @@
from __future__ import annotations
from functools import lru_cache
from pydantic import BaseSettings
class ServerSettings(BaseSettings):
"""Settings controlling server behavior.
The values listed here will be automatically modified based on the environment. e.g.
running with SERVER_BIND_ADDRESS=0.0.0.0 will cause the server to bind to all
interfaces.
https://fastapi.tiangolo.com/advanced/settings
"""
# WARNING: Be extremely cautious exposing the server to other machines. As there is
# no client/server workflow yet, security has not been a focus.
server_bind_address: str = "::1"
# If you for some reason change the port, you'll need to also update map.js.
server_port: int = 5000
@classmethod
@lru_cache
def get(cls) -> ServerSettings:
return cls()

View File

@ -21,4 +21,7 @@ ignore_missing_imports = True
[mypy-shapely.*]
# https://github.com/Toblerity/Shapely/issues/721
ignore_missing_imports = True
[mypy-uvicorn.*]
ignore_missing_imports = True

View File

@ -9,7 +9,7 @@ from typing import Optional
from PySide2 import QtWidgets
from PySide2.QtCore import Qt
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QApplication, QSplashScreen, QCheckBox
from PySide2.QtWidgets import QApplication, QCheckBox, QSplashScreen
from dcs.payloads import PayloadDirectories
from game import Game, VERSION, persistency
@ -18,6 +18,7 @@ from game.data.weapons import Pylon, Weapon, WeaponGroup
from game.db import FACTIONS
from game.dcs.aircrafttype import AircraftType
from game.profiling import logged_duration
from game.server import GameContext, Server
from game.settings import Settings
from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings
from qt_ui import (
@ -135,6 +136,7 @@ def run_ui(game: Optional[Game]) -> None:
# Apply CSS (need works)
GameUpdateSignal()
GameUpdateSignal.get_instance().game_loaded.connect(GameContext.set)
# Start window
window = QLiberationWindow(game)
@ -333,7 +335,8 @@ def main():
lint_weapon_data_for_aircraft(AircraftType.named(args.aircraft))
return
run_ui(game)
with Server().run_in_thread():
run_ui(game)
if __name__ == "__main__":

View File

@ -10,6 +10,7 @@ from PySide2.QtCore import QUrl
from PySide2.QtWebChannel import QWebChannel
from PySide2.QtWebEngineWidgets import (
QWebEnginePage,
QWebEngineSettings,
QWebEngineView,
)
@ -48,6 +49,11 @@ class QLiberationMap(QWebEngineView):
self.channel.registerObject("game", self.map_model)
self.page = LoggingWebPage(self)
# Required to allow "cross-origin" access from file:// scoped canvas.html to the
# localhost HTTP backend.
self.page.settings().setAttribute(
QWebEngineSettings.LocalContentCanAccessRemoteUrls, True
)
self.page.setWebChannel(self.channel)
self.page.load(
QUrl.fromLocalFile(str(Path("resources/ui/map/canvas.html").resolve()))

View File

@ -1,9 +1,8 @@
from PySide2.QtCore import Property, QObject, Signal
from game.server.leaflet import LeafletPoly, ShapelyUtil
from game.sim.combat.aircombat import AirCombat
from game.theater import ConflictTheater
from .leaflet import LeafletPoly
from .shapelyutil import ShapelyUtil
class AirCombatJs(QObject):

View File

@ -1,3 +0,0 @@
# Set to True to enable computing expensive debugging information. At the time of
# writing this only controls computing the waypoint placement zones.
ENABLE_EXPENSIVE_DEBUG_TOOLS = False

View File

@ -5,12 +5,12 @@ from typing import Optional
from PySide2.QtCore import Property, QObject, Signal, Slot
from dcs import Point
from game.server.leaflet import LeafletLatLon
from game.theater import ConflictTheater, ControlPoint, ControlPointStatus, LatLon
from game.utils import meters, nautical_miles
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from .leaflet import LeafletLatLon
MAX_SHIP_DISTANCE = nautical_miles(80)

View File

@ -8,12 +8,11 @@ from shapely.geometry import LineString, Point as ShapelyPoint
from game.ato import Flight, FlightWaypoint
from game.ato.flightstate import InFlight
from game.ato.flightwaypointtype import FlightWaypointType
from game.server.leaflet import LeafletLatLon, LeafletPoly, ShapelyUtil
from game.theater import ConflictTheater
from game.utils import meters
from gen.flights.flightplan import CasFlightPlan, PatrollingFlightPlan
from qt_ui.models import AtoModel
from .leaflet import LeafletLatLon, LeafletPoly
from .shapelyutil import ShapelyUtil
from .waypointjs import WaypointJs

View File

@ -4,10 +4,10 @@ from typing import List
from PySide2.QtCore import Property, QObject, Signal, Slot
from game.server.leaflet import LeafletLatLon
from game.theater import ConflictTheater, FrontLine
from game.utils import nautical_miles
from qt_ui.dialogs import Dialog
from .leaflet import LeafletLatLon
class FrontLineJs(QObject):

View File

@ -8,9 +8,9 @@ from dcs.vehicles import vehicle_map
from game import Game
from game.dcs.groundunittype import GroundUnitType
from game.server.leaflet import LeafletLatLon
from game.theater import TheaterGroundObject
from qt_ui.dialogs import Dialog
from qt_ui.widgets.map.model.leaflet import LeafletLatLon
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu

View File

@ -1,60 +0,0 @@
from __future__ import annotations
from pydantic import BaseModel, Field
from game import Game
from game.ato import Flight
from game.flightplan import HoldZoneGeometry
from .config import ENABLE_EXPENSIVE_DEBUG_TOOLS
from .leaflet import LeafletPoly
from .shapelyutil import ShapelyUtil
class HoldZonesJs(BaseModel):
home_bubble: LeafletPoly = Field(alias="homeBubble")
target_bubble: LeafletPoly = Field(alias="targetBubble")
join_bubble: LeafletPoly = Field(alias="joinBubble")
excluded_zones: list[LeafletPoly] = Field(alias="excludedZones")
permissible_zones: list[LeafletPoly] = Field(alias="permissibleZones")
preferred_lines: list[LeafletPoly] = Field(alias="preferredLines")
@classmethod
def empty(cls) -> HoldZonesJs:
return HoldZonesJs(
homeBubble=[],
targetBubble=[],
joinBubble=[],
excludedZones=[],
permissibleZones=[],
preferredLines=[],
)
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> HoldZonesJs:
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
return HoldZonesJs.empty()
target = flight.package.target
home = flight.departure
if flight.package.waypoints is None:
return HoldZonesJs.empty()
ip = flight.package.waypoints.ingress
join = flight.package.waypoints.join
geometry = HoldZoneGeometry(
target.position, home.position, ip, join, game.blue, game.theater
)
return HoldZonesJs(
homeBubble=ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater),
targetBubble=ShapelyUtil.poly_to_leaflet(
geometry.target_bubble, game.theater
),
joinBubble=ShapelyUtil.poly_to_leaflet(geometry.join_bubble, game.theater),
excludedZones=ShapelyUtil.polys_to_leaflet(
geometry.excluded_zones, game.theater
),
permissibleZones=ShapelyUtil.polys_to_leaflet(
geometry.permissible_zones, game.theater
),
preferredLines=ShapelyUtil.lines_to_leaflet(
geometry.preferred_lines, game.theater
),
)

View File

@ -1,37 +0,0 @@
from __future__ import annotations
from pydantic import BaseModel, Field
from game import Game
from game.ato import Flight
from game.flightplan import IpZoneGeometry
from .config import ENABLE_EXPENSIVE_DEBUG_TOOLS
from .leaflet import LeafletPoly
from .shapelyutil import ShapelyUtil
class IpZonesJs(BaseModel):
home_bubble: LeafletPoly = Field(alias="homeBubble")
ipBubble: LeafletPoly = Field(alias="ipBubble")
permissibleZone: LeafletPoly = Field(alias="permissibleZone")
safeZones: list[LeafletPoly] = Field(alias="safeZones")
@classmethod
def empty(cls) -> IpZonesJs:
return IpZonesJs(homeBubble=[], ipBubble=[], permissibleZone=[], safeZones=[])
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs:
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
return IpZonesJs.empty()
target = flight.package.target
home = flight.departure
geometry = IpZoneGeometry(target.position, home.position, game.blue)
return IpZonesJs(
homeBubble=ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater),
ipBubble=ShapelyUtil.poly_to_leaflet(geometry.ip_bubble, game.theater),
permissibleZone=ShapelyUtil.poly_to_leaflet(
geometry.permissible_zone, game.theater
),
safeZones=ShapelyUtil.polys_to_leaflet(geometry.safe_zones, game.theater),
)

View File

@ -1,58 +0,0 @@
from __future__ import annotations
from pydantic import Field
from pydantic.main import BaseModel
from game import Game
from game.ato import Flight
from game.flightplan import JoinZoneGeometry
from .config import ENABLE_EXPENSIVE_DEBUG_TOOLS
from .leaflet import LeafletPoly
from .shapelyutil import ShapelyUtil
class JoinZonesJs(BaseModel):
home_bubble: LeafletPoly = Field(alias="homeBubble")
target_bubble: LeafletPoly = Field(alias="targetBubble")
ip_bubble: LeafletPoly = Field(alias="ipBubble")
excluded_zones: list[LeafletPoly] = Field(alias="excludedZones")
permissible_zones: list[LeafletPoly] = Field(alias="permissibleZones")
preferred_lines: list[LeafletPoly] = Field(alias="preferredLines")
@classmethod
def empty(cls) -> JoinZonesJs:
return JoinZonesJs(
homeBubble=[],
targetBubble=[],
ipBubble=[],
excludedZones=[],
permissibleZones=[],
preferredLines=[],
)
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> JoinZonesJs:
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
return JoinZonesJs.empty()
target = flight.package.target
home = flight.departure
if flight.package.waypoints is None:
return JoinZonesJs.empty()
ip = flight.package.waypoints.ingress
geometry = JoinZoneGeometry(target.position, home.position, ip, game.blue)
return JoinZonesJs(
homeBubble=ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater),
targetBubble=ShapelyUtil.poly_to_leaflet(
geometry.target_bubble, game.theater
),
ipBubble=ShapelyUtil.poly_to_leaflet(geometry.ip_bubble, game.theater),
excludedZones=ShapelyUtil.polys_to_leaflet(
geometry.excluded_zones, game.theater
),
permissibleZones=ShapelyUtil.polys_to_leaflet(
geometry.permissible_zones, game.theater
),
preferredLines=ShapelyUtil.lines_to_leaflet(
geometry.preferred_lines, game.theater
),
)

View File

@ -1,4 +0,0 @@
from __future__ import annotations
LeafletLatLon = list[float]
LeafletPoly = list[LeafletLatLon]

View File

@ -1,6 +1,5 @@
from __future__ import annotations
import json
import logging
from typing import List, Optional, Tuple
@ -10,6 +9,7 @@ from dcs import Point
from game import Game
from game.ato.airtaaskingorder import AirTaskingOrder
from game.profiling import logged_duration
from game.server.leaflet import LeafletLatLon
from game.sim.combat import FrozenCombat
from game.sim.combat.aircombat import AirCombat
from game.sim.combat.atip import AtIp
@ -26,11 +26,7 @@ from .controlpointjs import ControlPointJs
from .flightjs import FlightJs
from .frontlinejs import FrontLineJs
from .groundobjectjs import GroundObjectJs
from .holdzonesjs import HoldZonesJs
from .ipcombatjs import IpCombatJs
from .ipzonesjs import IpZonesJs
from .joinzonesjs import JoinZonesJs
from .leaflet import LeafletLatLon
from .mapzonesjs import MapZonesJs
from .navmeshjs import NavMeshJs
from .samcombatjs import SamCombatJs
@ -69,12 +65,10 @@ class MapModel(QObject):
navmeshesChanged = Signal()
mapZonesChanged = Signal()
unculledZonesChanged = Signal()
ipZonesChanged = Signal()
joinZonesChanged = Signal()
holdZonesChanged = Signal()
airCombatsChanged = Signal()
samCombatsChanged = Signal()
ipCombatsChanged = Signal()
selectedFlightChanged = Signal(str)
def __init__(self, game_model: GameModel, sim_controller: SimController) -> None:
super().__init__()
@ -91,9 +85,6 @@ class MapModel(QObject):
self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = []
self._ip_zones = IpZonesJs.empty()
self._join_zones = JoinZonesJs.empty()
self._hold_zones = HoldZonesJs.empty()
self._selected_flight_index: Optional[Tuple[int, int]] = None
self._air_combats = []
self._sam_combats = []
@ -128,7 +119,6 @@ class MapModel(QObject):
self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = []
self._ip_zones = IpZonesJs.empty()
self._air_combats = []
self._sam_combats = []
self._ip_combats = []
@ -155,7 +145,6 @@ class MapModel(QObject):
else:
self._selected_flight_index = index, 0
self.select_current_flight()
self.reset_debug_zones()
def set_flight_selection(self, index: int) -> None:
self.deselect_current_flight()
@ -174,7 +163,6 @@ class MapModel(QObject):
self._selected_flight_index = self._selected_flight_index[0], None
self._selected_flight_index = self._selected_flight_index[0], index
self.select_current_flight()
self.reset_debug_zones()
@property
def _selected_flight(self) -> Optional[FlightJs]:
@ -193,8 +181,10 @@ class MapModel(QObject):
def select_current_flight(self):
flight = self._selected_flight
if flight is None:
self.selectedFlightChanged.emit(None)
return None
flight.set_selected(True)
self.selectedFlightChanged.emit(str(flight.flight.id))
@staticmethod
def leaflet_coord_for(point: Point, theater: ConflictTheater) -> LeafletLatLon:
@ -249,23 +239,6 @@ class MapModel(QObject):
self.game.blue.ato, blue=True
) | self._flights_in_ato(self.game.red.ato, blue=False)
self.flightsChanged.emit()
self.reset_debug_zones()
def reset_debug_zones(self) -> None:
selected_flight = None
if self._selected_flight is not None:
selected_flight = self._selected_flight.flight
if selected_flight is None:
self._ip_zones = IpZonesJs.empty()
self._join_zones = JoinZonesJs.empty()
self._hold_zones = HoldZonesJs.empty()
else:
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
self._join_zones = JoinZonesJs.for_flight(selected_flight, self.game)
self._hold_zones = HoldZonesJs.for_flight(selected_flight, self.game)
self.ipZonesChanged.emit()
self.joinZonesChanged.emit()
self.holdZonesChanged.emit()
@Property(list, notify=flightsChanged)
def flights(self) -> list[FlightJs]:
@ -397,20 +370,6 @@ class MapModel(QObject):
def unculledZones(self) -> list[UnculledZone]:
return self._unculled_zones
@Property(str, notify=ipZonesChanged)
def ipZones(self) -> str:
return json.dumps(self._ip_zones.dict(by_alias=True))
@Property(str, notify=joinZonesChanged)
def joinZones(self) -> str:
# Must be dumped as a string and deserialized in js because QWebChannel can't
# handle a dict. Can be cleaned up by switching from QWebChannel to FastAPI.
return json.dumps(self._join_zones.dict(by_alias=True))
@Property(str, notify=holdZonesChanged)
def holdZones(self) -> str:
return json.dumps(self._hold_zones.dict(by_alias=True))
def reset_combats(self) -> None:
self._air_combats = []
self._sam_combats = []

View File

@ -3,8 +3,7 @@ from __future__ import annotations
from PySide2.QtCore import Property, QObject, Signal
from game import Game
from .leaflet import LeafletPoly
from .shapelyutil import ShapelyUtil
from game.server.leaflet import LeafletPoly, ShapelyUtil
class MapZonesJs(QObject):

View File

@ -4,8 +4,8 @@ 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 .leaflet import LeafletPoly
from .navmeshpolyjs import NavMeshPolyJs

View File

@ -3,9 +3,8 @@ 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
from .leaflet import LeafletPoly
from .shapelyutil import ShapelyUtil
class NavMeshPolyJs(QObject):

View File

@ -5,9 +5,9 @@ from typing import List
from PySide2.QtCore import Property, QObject, Signal
from game import Game
from game.server.leaflet import LeafletLatLon
from game.theater import ControlPoint
from game.transfers import MultiGroupTransport, TransportMap
from .leaflet import LeafletLatLon
class SupplyRouteJs(QObject):

View File

@ -2,10 +2,9 @@ from __future__ import annotations
from PySide2.QtCore import Property, QObject, Signal
from game.server.leaflet import LeafletPoly, ShapelyUtil
from game.theater import ConflictTheater
from game.threatzones import ThreatZones
from .leaflet import LeafletPoly
from .shapelyutil import ShapelyUtil
class ThreatZonesJs(QObject):

View File

@ -5,7 +5,7 @@ from typing import Iterator
from PySide2.QtCore import Property, QObject, Signal
from game import Game
from .leaflet import LeafletLatLon
from game.server.leaflet import LeafletLatLon
class UnculledZone(QObject):

View File

@ -7,10 +7,10 @@ from PySide2.QtCore import Property, QObject, Signal, Slot
from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.server.leaflet import LeafletLatLon
from game.theater import ConflictTheater, LatLon
from gen.flights.flightplan import FlightPlan
from qt_ui.models import AtoModel
from .leaflet import LeafletLatLon
if TYPE_CHECKING:
from .flightjs import FlightJs

View File

@ -1,4 +1,6 @@
altgraph==0.17.2
anyio==3.5.0
asgiref==3.5.0
atomicwrites==1.4.0
attrs==21.4.0
black==22.1.0
@ -8,9 +10,13 @@ click==8.0.3
colorama==0.4.4
distlib==0.3.4
Faker==12.3.0
fastapi==0.73.0
filelock==3.4.2
future==0.18.2
h11==0.13.0
httptools==0.3.0
identify==2.4.9
idna==3.3
iniconfig==1.1.1
Jinja2==3.0.3
MarkupSafe==2.0.1
@ -34,11 +40,14 @@ pyproj==3.3.0
PySide2==5.15.2.1
pytest==7.0.1
python-dateutil==2.8.2
python-dotenv==0.19.2
pywin32-ctypes==0.2.0
PyYAML==6.0
./wheels/Shapely-1.8.0-cp310-cp310-win_amd64.whl
shiboken2==5.15.2.1
six==1.16.0
sniffio==1.2.0
starlette==0.17.1
tabulate==0.8.9
text-unidecode==1.3
toml==0.10.2
@ -49,4 +58,7 @@ types-Pillow==9.0.6
types-PyYAML==6.0.4
types-tabulate==0.8.5
typing_extensions==4.0.1
uvicorn==0.17.4
virtualenv==20.13.1
watchgod==0.7
websockets==10.1

View File

@ -1,6 +1,12 @@
// Won't actually enable anything unless the same property is set in
// mapmodel.py.
const ENABLE_EXPENSIVE_DEBUG_TOOLS = false;
// Must be kept in sync with game.server.settings.ServerSettings.
const HTTP_BACKEND = "http://[::1]:5000";
function getJson(endpoint) {
return fetch(`${HTTP_BACKEND}${endpoint}`).then((response) =>
response.json()
);
}
const Colors = Object.freeze({
Blue: "#0084ff",
@ -350,18 +356,29 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.navmeshesChanged.connect(drawNavmeshes);
game.mapZonesChanged.connect(drawMapZones);
game.unculledZonesChanged.connect(drawUnculledZones);
game.ipZonesChanged.connect(drawIpZones);
game.joinZonesChanged.connect(drawJoinZones);
game.holdZonesChanged.connect(drawHoldZones);
game.airCombatsChanged.connect(drawCombat);
game.samCombatsChanged.connect(drawCombat);
game.ipCombatsChanged.connect(drawCombat);
game.selectedFlightChanged.connect(updateSelectedFlight);
});
function recenterMap(center) {
map.setView(center, 8, { animate: true, duration: 1 });
}
function updateSelectedFlight(id) {
if (id == null) {
holdZones.clearLayers();
ipZones.clearLayers();
joinZones.clearLayers();
return;
}
drawHoldZones(id);
drawIpZones(id);
drawJoinZones(id);
}
class ControlPoint {
constructor(cp) {
this.cp = cp;
@ -1099,142 +1116,142 @@ function drawUnculledZones() {
}
}
function drawIpZones() {
function drawIpZones(id) {
ipZones.clearLayers();
if (!ENABLE_EXPENSIVE_DEBUG_TOOLS) {
return;
}
const iz = JSON.parse(game.ipZones);
L.polygon(iz.homeBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(ipZones);
L.polygon(iz.ipBubble, {
color: "#bb89ff",
fillOpacity: 0.1,
interactive: false,
}).addTo(ipZones);
L.polygon(iz.permissibleZone, {
color: "#ffffff",
fillOpacity: 0.1,
interactive: false,
}).addTo(ipZones);
for (const zone of iz.safeZones) {
L.polygon(zone, {
color: Colors.Green,
getJson(`/debug/waypoint-geometries/ip/${id}`).then((iz) => {
L.polygon(iz.homeBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(ipZones);
}
L.polygon(iz.ipBubble, {
color: "#bb89ff",
fillOpacity: 0.1,
interactive: false,
}).addTo(ipZones);
L.polygon(iz.permissibleZone, {
color: "#ffffff",
fillOpacity: 0.1,
interactive: false,
}).addTo(ipZones);
for (const zone of iz.safeZones) {
L.polygon(zone, {
color: Colors.Green,
fillOpacity: 0.1,
interactive: false,
}).addTo(ipZones);
}
});
}
function drawJoinZones() {
function drawJoinZones(id) {
joinZones.clearLayers();
if (!ENABLE_EXPENSIVE_DEBUG_TOOLS) {
return;
}
const jz = JSON.parse(game.joinZones);
L.polygon(jz.homeBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(joinZones);
L.polygon(jz.targetBubble, {
color: "#bb89ff",
fillOpacity: 0.1,
interactive: false,
}).addTo(joinZones);
L.polygon(jz.ipBubble, {
color: "#ffffff",
fillOpacity: 0.1,
interactive: false,
}).addTo(joinZones);
for (const zone of jz.excludedZones) {
L.polygon(zone, {
color: "#ffa500",
fillOpacity: 0.2,
stroke: false,
getJson(`/debug/waypoint-geometries/join/${id}`).then((jz) => {
L.polygon(jz.homeBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(joinZones);
}
for (const zone of jz.permissibleZones) {
L.polygon(zone, {
color: Colors.Green,
L.polygon(jz.targetBubble, {
color: "#bb89ff",
fillOpacity: 0.1,
interactive: false,
}).addTo(joinZones);
}
for (const line of jz.preferredLines) {
L.polyline(line, {
color: Colors.Green,
L.polygon(jz.ipBubble, {
color: "#ffffff",
fillOpacity: 0.1,
interactive: false,
}).addTo(joinZones);
}
for (const zone of jz.excludedZones) {
L.polygon(zone, {
color: "#ffa500",
fillOpacity: 0.2,
stroke: false,
interactive: false,
}).addTo(joinZones);
}
for (const zone of jz.permissibleZones) {
L.polygon(zone, {
color: Colors.Green,
interactive: false,
}).addTo(joinZones);
}
for (const line of jz.preferredLines) {
L.polyline(line, {
color: Colors.Green,
interactive: false,
}).addTo(joinZones);
}
});
}
function drawHoldZones() {
function drawHoldZones(id) {
holdZones.clearLayers();
if (!ENABLE_EXPENSIVE_DEBUG_TOOLS) {
return;
}
const hz = JSON.parse(game.holdZones);
L.polygon(hz.homeBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(holdZones);
L.polygon(hz.targetBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(holdZones);
L.polygon(hz.joinBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(holdZones);
for (const zone of hz.excludedZones) {
L.polygon(zone, {
color: "#ffa500",
fillOpacity: 0.2,
stroke: false,
getJson(`/debug/waypoint-geometries/hold/${id}`).then((hz) => {
L.polygon(hz.homeBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(holdZones);
}
for (const zone of hz.permissibleZones) {
L.polygon(zone, {
color: Colors.Green,
L.polygon(hz.targetBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(holdZones);
}
for (const line of hz.preferredLines) {
L.polyline(line, {
color: Colors.Green,
L.polygon(hz.joinBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(holdZones);
}
for (const zone of hz.excludedZones) {
L.polygon(zone, {
color: "#ffa500",
fillOpacity: 0.2,
stroke: false,
interactive: false,
}).addTo(holdZones);
}
for (const zone of hz.permissibleZones) {
L.polygon(zone, {
color: Colors.Green,
interactive: false,
}).addTo(holdZones);
}
for (const line of hz.preferredLines) {
L.polyline(line, {
color: Colors.Green,
interactive: false,
}).addTo(holdZones);
}
});
}
function drawCombat() {
@ -1276,9 +1293,6 @@ function drawInitialMap() {
drawNavmeshes();
drawMapZones();
drawUnculledZones();
drawIpZones();
drawJoinZones();
drawHoldZones();
drawCombat();
}