mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
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:
parent
1df31b2496
commit
350f08be2f
@ -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
2
game/server/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .dependencies import GameContext
|
||||
from .server import Server
|
||||
6
game/server/app.py
Normal file
6
game/server/app.py
Normal file
@ -0,0 +1,6 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from . import debuggeometries
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(debuggeometries.router)
|
||||
1
game/server/debuggeometries/__init__.py
Normal file
1
game/server/debuggeometries/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .routes import router
|
||||
126
game/server/debuggeometries/models.py
Normal file
126
game/server/debuggeometries/models.py
Normal 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
|
||||
),
|
||||
)
|
||||
38
game/server/debuggeometries/routes.py
Normal file
38
game/server/debuggeometries/routes.py
Normal 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)
|
||||
15
game/server/dependencies.py
Normal file
15
game/server/dependencies.py
Normal 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
|
||||
@ -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
35
game/server/server.py
Normal 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
28
game/server/settings.py
Normal 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()
|
||||
3
mypy.ini
3
mypy.ini
@ -22,3 +22,6 @@ ignore_missing_imports = True
|
||||
[mypy-shapely.*]
|
||||
# https://github.com/Toblerity/Shapely/issues/721
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-uvicorn.*]
|
||||
ignore_missing_imports = True
|
||||
@ -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,6 +335,7 @@ def main():
|
||||
lint_weapon_data_for_aircraft(AircraftType.named(args.aircraft))
|
||||
return
|
||||
|
||||
with Server().run_in_thread():
|
||||
run_ui(game)
|
||||
|
||||
|
||||
|
||||
@ -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()))
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
),
|
||||
)
|
||||
@ -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),
|
||||
)
|
||||
@ -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
|
||||
),
|
||||
)
|
||||
@ -1,4 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
LeafletLatLon = list[float]
|
||||
LeafletPoly = list[LeafletLatLon]
|
||||
@ -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 = []
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,15 +1116,14 @@ function drawUnculledZones() {
|
||||
}
|
||||
}
|
||||
|
||||
function drawIpZones() {
|
||||
function drawIpZones(id) {
|
||||
ipZones.clearLayers();
|
||||
|
||||
if (!ENABLE_EXPENSIVE_DEBUG_TOOLS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iz = JSON.parse(game.ipZones);
|
||||
|
||||
getJson(`/debug/waypoint-geometries/ip/${id}`).then((iz) => {
|
||||
L.polygon(iz.homeBubble, {
|
||||
color: Colors.Highlight,
|
||||
fillOpacity: 0.1,
|
||||
@ -1133,17 +1149,17 @@ function drawIpZones() {
|
||||
interactive: false,
|
||||
}).addTo(ipZones);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawJoinZones() {
|
||||
function drawJoinZones(id) {
|
||||
joinZones.clearLayers();
|
||||
|
||||
if (!ENABLE_EXPENSIVE_DEBUG_TOOLS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jz = JSON.parse(game.joinZones);
|
||||
|
||||
getJson(`/debug/waypoint-geometries/join/${id}`).then((jz) => {
|
||||
L.polygon(jz.homeBubble, {
|
||||
color: Colors.Highlight,
|
||||
fillOpacity: 0.1,
|
||||
@ -1184,17 +1200,17 @@ function drawJoinZones() {
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawHoldZones() {
|
||||
function drawHoldZones(id) {
|
||||
holdZones.clearLayers();
|
||||
|
||||
if (!ENABLE_EXPENSIVE_DEBUG_TOOLS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hz = JSON.parse(game.holdZones);
|
||||
|
||||
getJson(`/debug/waypoint-geometries/hold/${id}`).then((hz) => {
|
||||
L.polygon(hz.homeBubble, {
|
||||
color: Colors.Highlight,
|
||||
fillOpacity: 0.1,
|
||||
@ -1235,6 +1251,7 @@ function drawHoldZones() {
|
||||
interactive: false,
|
||||
}).addTo(holdZones);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawCombat() {
|
||||
@ -1276,9 +1293,6 @@ function drawInitialMap() {
|
||||
drawNavmeshes();
|
||||
drawMapZones();
|
||||
drawUnculledZones();
|
||||
drawIpZones();
|
||||
drawJoinZones();
|
||||
drawHoldZones();
|
||||
drawCombat();
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user