mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Move FlightJs out of MapModel.
This commit is contained in:
parent
ad0d3412fb
commit
45e76e12b6
@ -17,8 +17,13 @@ class AirTaskingOrder:
|
||||
|
||||
def remove_package(self, package: Package) -> None:
|
||||
"""Removes a package from the ATO."""
|
||||
# Remove all the flights individually so the database gets updated.
|
||||
for flight in list(package.flights):
|
||||
package.remove_flight(flight)
|
||||
self.packages.remove(package)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Removes all packages from the ATO."""
|
||||
self.packages.clear()
|
||||
# Remove all packages individually so the database gets updated.
|
||||
for package in self.packages:
|
||||
self.remove_package(package)
|
||||
|
||||
@ -22,7 +22,7 @@ class Navigating(InFlight):
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
super().on_game_tick(events, time, duration)
|
||||
events.update_flight(self.flight, self.estimate_position())
|
||||
events.update_flight_position(self.flight, self.estimate_position())
|
||||
|
||||
def progress(self) -> float:
|
||||
return (
|
||||
|
||||
@ -129,6 +129,9 @@ class Package:
|
||||
"""Removes a flight from the package."""
|
||||
self.flights.remove(flight)
|
||||
self._db.remove(flight.id)
|
||||
flight.return_pilots_and_aircraft()
|
||||
if flight.cargo is not None:
|
||||
flight.cargo.transport = None
|
||||
if not self.flights:
|
||||
self.waypoints = None
|
||||
|
||||
|
||||
@ -92,5 +92,4 @@ class PackageBuilder:
|
||||
"""Returns any planned flights to the inventory."""
|
||||
flights = list(self.package.flights)
|
||||
for flight in flights:
|
||||
flight.return_pilots_and_aircraft()
|
||||
self.package.remove_flight(flight)
|
||||
|
||||
@ -6,6 +6,7 @@ from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.server.combat.models import FrozenCombatJs
|
||||
from game.server.flights.models import FlightJs
|
||||
from game.server.leaflet import LeafletLatLon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -14,18 +15,24 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class GameUpdateEventsJs(BaseModel):
|
||||
updated_flights: dict[UUID, LeafletLatLon]
|
||||
new_combats: list[FrozenCombatJs] = []
|
||||
updated_combats: list[FrozenCombatJs] = []
|
||||
navmesh_updates: set[bool] = set()
|
||||
unculled_zones_updated: bool = False
|
||||
threat_zones_updated: bool = False
|
||||
updated_flight_positions: dict[UUID, LeafletLatLon]
|
||||
new_combats: list[FrozenCombatJs]
|
||||
updated_combats: list[FrozenCombatJs]
|
||||
navmesh_updates: set[bool]
|
||||
unculled_zones_updated: bool
|
||||
threat_zones_updated: bool
|
||||
new_flights: list[FlightJs]
|
||||
updated_flights: set[UUID]
|
||||
deleted_flights: set[UUID]
|
||||
selected_flight: UUID | None
|
||||
deselected_flight: bool
|
||||
|
||||
@classmethod
|
||||
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
|
||||
return GameUpdateEventsJs(
|
||||
updated_flights={
|
||||
f[0].id: f[1].latlng().as_list() for f in events.updated_flights
|
||||
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
|
||||
@ -37,4 +44,9 @@ class GameUpdateEventsJs(BaseModel):
|
||||
navmesh_updates=events.navmesh_updates,
|
||||
unculled_zones_updated=events.unculled_zones_updated,
|
||||
threat_zones_updated=events.threat_zones_updated,
|
||||
new_flights=[FlightJs.for_flight(f) for f in events.new_flights],
|
||||
updated_flights=events.updated_flights,
|
||||
deleted_flights=events.deleted_flights,
|
||||
selected_flight=events.selected_flight,
|
||||
deselected_flight=events.deselected_flight,
|
||||
)
|
||||
|
||||
26
game/server/flights/models.py
Normal file
26
game/server/flights/models.py
Normal file
@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from dcs.mapping import LatLng
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.ato import Flight
|
||||
from game.ato.flightstate import InFlight
|
||||
|
||||
|
||||
class FlightJs(BaseModel):
|
||||
id: UUID
|
||||
blue: bool
|
||||
position: LatLng | None
|
||||
|
||||
@staticmethod
|
||||
def for_flight(flight: Flight) -> FlightJs:
|
||||
# Don't provide a location for aircraft that aren't in the air. Later we can
|
||||
# expand the model to include the state data for the UI so that it can make its
|
||||
# own decisions about whether or not to draw the aircraft, but for now we'll
|
||||
# filter here.
|
||||
position = None
|
||||
if isinstance(flight.state, InFlight):
|
||||
position = flight.position().latlng()
|
||||
return FlightJs(id=flight.id, blue=flight.blue, position=position)
|
||||
@ -4,13 +4,30 @@ from fastapi import APIRouter, Depends
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
|
||||
from game import Game
|
||||
from game.server import GameContext
|
||||
from game.server.leaflet import LeafletPoly, ShapelyUtil
|
||||
from game.ato.flightplan import CasFlightPlan, PatrollingFlightPlan
|
||||
from game.server import GameContext
|
||||
from game.server.flights.models import FlightJs
|
||||
from game.server.leaflet import LeafletPoly, ShapelyUtil
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/flights")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_flights(game: Game = Depends(GameContext.get)) -> 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))
|
||||
return flights
|
||||
|
||||
|
||||
@router.get("/{flight_id}")
|
||||
def get_flight(flight_id: UUID, game: Game = Depends(GameContext.get)) -> FlightJs:
|
||||
flight = game.db.flights.get(flight_id)
|
||||
return FlightJs.for_flight(flight)
|
||||
|
||||
|
||||
@router.get("/{flight_id}/commit-boundary")
|
||||
def commit_boundary(
|
||||
flight_id: UUID, game: Game = Depends(GameContext.get)
|
||||
|
||||
@ -1,55 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from dcs import Point
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import Flight
|
||||
from game.ato import Flight, Package
|
||||
from game.sim.combat import FrozenCombat
|
||||
|
||||
|
||||
@dataclass
|
||||
class GameUpdateEvents:
|
||||
def __init__(self) -> None:
|
||||
self.simulation_complete = False
|
||||
self.new_combats: list[FrozenCombat] = []
|
||||
self.updated_combats: list[FrozenCombat] = []
|
||||
self.updated_flights: list[tuple[Flight, Point]] = []
|
||||
self.navmesh_updates: set[bool] = set()
|
||||
self.unculled_zones_updated: bool = False
|
||||
self.threat_zones_updated: bool = False
|
||||
simulation_complete = False
|
||||
new_combats: list[FrozenCombat] = field(default_factory=list)
|
||||
updated_combats: list[FrozenCombat] = field(default_factory=list)
|
||||
updated_flight_positions: list[tuple[Flight, Point]] = field(default_factory=list)
|
||||
navmesh_updates: set[bool] = field(default_factory=set)
|
||||
unculled_zones_updated: bool = False
|
||||
threat_zones_updated: bool = False
|
||||
new_flights: set[Flight] = field(default_factory=set)
|
||||
updated_flights: set[UUID] = field(default_factory=set)
|
||||
deleted_flights: set[UUID] = field(default_factory=set)
|
||||
selected_flight: UUID | None = None
|
||||
deselected_flight: bool = False
|
||||
|
||||
@property
|
||||
def empty(self) -> bool:
|
||||
return not any(
|
||||
[
|
||||
self.simulation_complete,
|
||||
self.new_combats,
|
||||
self.updated_combats,
|
||||
self.updated_flights,
|
||||
self.navmesh_updates,
|
||||
self.unculled_zones_updated,
|
||||
self.threat_zones_updated,
|
||||
]
|
||||
)
|
||||
return self == GameUpdateEvents()
|
||||
|
||||
def complete_simulation(self) -> None:
|
||||
def complete_simulation(self) -> GameUpdateEvents:
|
||||
self.simulation_complete = True
|
||||
return self
|
||||
|
||||
def new_combat(self, combat: FrozenCombat) -> None:
|
||||
def new_combat(self, combat: FrozenCombat) -> GameUpdateEvents:
|
||||
self.new_combats.append(combat)
|
||||
return self
|
||||
|
||||
def update_combat(self, combat: FrozenCombat) -> None:
|
||||
def update_combat(self, combat: FrozenCombat) -> GameUpdateEvents:
|
||||
self.updated_combats.append(combat)
|
||||
return self
|
||||
|
||||
def update_flight(self, flight: Flight, new_position: Point) -> None:
|
||||
self.updated_flights.append((flight, new_position))
|
||||
def update_flight_position(
|
||||
self, flight: Flight, new_position: Point
|
||||
) -> GameUpdateEvents:
|
||||
self.updated_flight_positions.append((flight, new_position))
|
||||
return self
|
||||
|
||||
def update_navmesh(self, player: bool) -> None:
|
||||
def update_navmesh(self, player: bool) -> GameUpdateEvents:
|
||||
self.navmesh_updates.add(player)
|
||||
return self
|
||||
|
||||
def update_unculled_zones(self) -> None:
|
||||
def update_unculled_zones(self) -> GameUpdateEvents:
|
||||
self.unculled_zones_updated = True
|
||||
return self
|
||||
|
||||
def update_threat_zones(self) -> None:
|
||||
def update_threat_zones(self) -> GameUpdateEvents:
|
||||
self.threat_zones_updated = True
|
||||
return self
|
||||
|
||||
def new_flight(self, flight: Flight) -> GameUpdateEvents:
|
||||
self.new_flights.add(flight)
|
||||
return self
|
||||
|
||||
def update_flight(self, flight: Flight) -> GameUpdateEvents:
|
||||
self.updated_flights.add(flight.id)
|
||||
return self
|
||||
|
||||
def update_flights_in_package(self, package: Package) -> GameUpdateEvents:
|
||||
self.updated_flights.update({f.id for f in package.flights})
|
||||
return self
|
||||
|
||||
def delete_flight(self, flight: Flight) -> GameUpdateEvents:
|
||||
self.deleted_flights.add(flight.id)
|
||||
return self
|
||||
|
||||
def delete_flights_in_package(self, package: Package) -> GameUpdateEvents:
|
||||
self.deleted_flights.update({f.id for f in package.flights})
|
||||
return self
|
||||
|
||||
def select_flight(self, flight: Flight) -> GameUpdateEvents:
|
||||
self.selected_flight = flight.id
|
||||
self.deselected_flight = False
|
||||
return self
|
||||
|
||||
def deselect_flight(self) -> GameUpdateEvents:
|
||||
self.deselected_flight = True
|
||||
self.selected_flight = None
|
||||
return self
|
||||
|
||||
@ -9,8 +9,8 @@ from typing import Optional, Sequence, TYPE_CHECKING
|
||||
from faker import Faker
|
||||
|
||||
from game.ato import Flight, FlightType, Package
|
||||
from game.settings import AutoAtoBehavior, Settings
|
||||
from game.ato.flightplan import FlightPlanBuilder
|
||||
from game.settings import AutoAtoBehavior, Settings
|
||||
from .pilot import Pilot, PilotStatus
|
||||
from ..db.database import Database
|
||||
from ..utils import meters
|
||||
@ -381,7 +381,6 @@ class Squadron:
|
||||
for flight in list(package.flights):
|
||||
if flight.squadron == self and flight.flight_type is FlightType.FERRY:
|
||||
package.remove_flight(flight)
|
||||
flight.return_pilots_and_aircraft()
|
||||
if not package.flights:
|
||||
self.coalition.ato.remove_package(package)
|
||||
|
||||
|
||||
@ -40,7 +40,10 @@ from typing import Generic, Iterator, List, Optional, Sequence, TYPE_CHECKING, T
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from game.ato.ai_flight_planner_db import aircraft_for_task
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.flightplan import FlightPlanBuilder
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.package import Package
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
@ -53,9 +56,6 @@ from game.theater.transitnetwork import (
|
||||
TransitNetwork,
|
||||
)
|
||||
from game.utils import meters, nautical_miles
|
||||
from game.ato.ai_flight_planner_db import aircraft_for_task
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.ato.flightplan import FlightPlanBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -635,7 +635,6 @@ class PendingTransfers:
|
||||
flight.package.remove_flight(flight)
|
||||
if not flight.package.flights:
|
||||
self.game.ato_for(self.player).remove_package(flight.package)
|
||||
flight.return_pilots_and_aircraft()
|
||||
|
||||
@cancel_transport.register
|
||||
def _cancel_transport_convoy(
|
||||
|
||||
@ -169,9 +169,6 @@ class PackageModel(QAbstractListModel):
|
||||
"""Removes the given flight from the package."""
|
||||
index = self.package.flights.index(flight)
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
if flight.cargo is not None:
|
||||
flight.cargo.transport = None
|
||||
flight.return_pilots_and_aircraft()
|
||||
self.package.remove_flight(flight)
|
||||
self.endRemoveRows()
|
||||
self.update_tot()
|
||||
@ -253,6 +250,8 @@ class AtoModel(QAbstractListModel):
|
||||
"""Adds a package to the ATO."""
|
||||
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
||||
self.ato.add_package(package)
|
||||
# We do not need to send events for new flights in the package here. Events were
|
||||
# already sent when the flights were added to the in-progress package.
|
||||
self.endInsertRows()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.client_slots_changed.emit()
|
||||
@ -264,14 +263,11 @@ class AtoModel(QAbstractListModel):
|
||||
|
||||
def delete_package(self, package: Package) -> None:
|
||||
"""Removes the given package from the ATO."""
|
||||
EventStream.put_nowait(GameUpdateEvents().delete_flights_in_package(package))
|
||||
self.package_models.release(package)
|
||||
index = self.ato.packages.index(package)
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
self.ato.remove_package(package)
|
||||
for flight in package.flights:
|
||||
flight.return_pilots_and_aircraft()
|
||||
if flight.cargo is not None:
|
||||
flight.cargo.transport = None
|
||||
self.endRemoveRows()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.client_slots_changed.emit()
|
||||
@ -280,9 +276,7 @@ class AtoModel(QAbstractListModel):
|
||||
def on_packages_changed(self) -> None:
|
||||
if self.game is not None:
|
||||
self.game.compute_unculled_zones()
|
||||
events = GameUpdateEvents()
|
||||
events.update_unculled_zones()
|
||||
EventStream.put_nowait(events)
|
||||
EventStream.put_nowait(GameUpdateEvents().update_unculled_zones())
|
||||
|
||||
def package_at_index(self, index: QModelIndex) -> Package:
|
||||
"""Returns the package at the given index."""
|
||||
|
||||
@ -27,7 +27,8 @@ from PySide2.QtWidgets import (
|
||||
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.package import Package
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from game.server import EventStream
|
||||
from game.sim import GameUpdateEvents
|
||||
from ..delegates import TwoColumnRowDelegate
|
||||
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
||||
|
||||
@ -128,8 +129,10 @@ class QFlightList(QListView):
|
||||
)
|
||||
|
||||
def delete_flight(self, index: QModelIndex) -> None:
|
||||
EventStream.put_nowait(
|
||||
GameUpdateEvents().delete_flight(self.package_model.flight_at_index(index))
|
||||
)
|
||||
self.package_model.delete_flight_at_index(index)
|
||||
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||
|
||||
def contextMenuEvent(self, event: QContextMenuEvent) -> None:
|
||||
index = self.indexAt(event.pos())
|
||||
@ -209,13 +212,13 @@ class QFlightPanel(QGroupBox):
|
||||
self.delete_button.setEnabled(enabled)
|
||||
self.change_map_flight_selection(index)
|
||||
|
||||
@staticmethod
|
||||
def change_map_flight_selection(index: QModelIndex) -> None:
|
||||
def change_map_flight_selection(self, index: QModelIndex) -> None:
|
||||
events = GameUpdateEvents()
|
||||
if not index.isValid():
|
||||
GameUpdateSignal.get_instance().select_flight(None)
|
||||
return
|
||||
|
||||
GameUpdateSignal.get_instance().select_flight(index.row())
|
||||
events.deselect_flight()
|
||||
else:
|
||||
events.select_flight(self.package_model.flight_at_index(index))
|
||||
EventStream.put_nowait(events)
|
||||
|
||||
def on_edit(self) -> None:
|
||||
"""Opens the flight edit dialog."""
|
||||
@ -303,7 +306,6 @@ class QPackageList(QListView):
|
||||
|
||||
def delete_package(self, index: QModelIndex) -> None:
|
||||
self.ato_model.delete_package_at_index(index)
|
||||
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||
|
||||
def on_new_packages(self, _parent: QModelIndex, first: int, _last: int) -> None:
|
||||
# Select the newly created pacakges. This should only ever happen due to
|
||||
@ -390,14 +392,18 @@ class QPackagePanel(QGroupBox):
|
||||
|
||||
def change_map_package_selection(self, index: QModelIndex) -> None:
|
||||
if not index.isValid():
|
||||
GameUpdateSignal.get_instance().select_package(None)
|
||||
EventStream.put_nowait(GameUpdateEvents().deselect_flight())
|
||||
return
|
||||
|
||||
package = self.ato_model.get_package_model(index)
|
||||
if package.rowCount() == 0:
|
||||
GameUpdateSignal.get_instance().select_package(None)
|
||||
EventStream.put_nowait(GameUpdateEvents().deselect_flight())
|
||||
else:
|
||||
GameUpdateSignal.get_instance().select_package(index.row())
|
||||
EventStream.put_nowait(
|
||||
GameUpdateEvents().select_flight(
|
||||
package.flight_at_index(package.index(0))
|
||||
)
|
||||
)
|
||||
|
||||
def on_edit(self) -> None:
|
||||
"""Opens the package edit dialog."""
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide2.QtCore import Property, QObject, Signal, Slot
|
||||
|
||||
from game.ato import Flight
|
||||
from game.ato.flightstate import InFlight
|
||||
from game.server.leaflet import LeafletLatLon
|
||||
from qt_ui.models import AtoModel
|
||||
|
||||
|
||||
class FlightJs(QObject):
|
||||
idChanged = Signal()
|
||||
positionChanged = Signal()
|
||||
blueChanged = Signal()
|
||||
selectedChanged = Signal()
|
||||
|
||||
def __init__(self, flight: Flight, selected: bool, ato_model: AtoModel) -> None:
|
||||
super().__init__()
|
||||
self.flight = flight
|
||||
self._selected = selected
|
||||
self.ato_model = ato_model
|
||||
|
||||
@Property(str, notify=idChanged)
|
||||
def id(self) -> str:
|
||||
return str(self.flight.id)
|
||||
|
||||
@Property(list, notify=positionChanged)
|
||||
def position(self) -> LeafletLatLon:
|
||||
if isinstance(self.flight.state, InFlight):
|
||||
return self.flight.state.estimate_position().latlng().as_list()
|
||||
return []
|
||||
|
||||
@Property(bool, notify=blueChanged)
|
||||
def blue(self) -> bool:
|
||||
return self.flight.departure.captured
|
||||
|
||||
@Property(bool, notify=selectedChanged)
|
||||
def selected(self) -> bool:
|
||||
return self._selected
|
||||
|
||||
@Slot(result=bool)
|
||||
def flightIsInAto(self) -> bool:
|
||||
if self.flight.package not in self.flight.squadron.coalition.ato.packages:
|
||||
return False
|
||||
if self.flight not in self.flight.package.flights:
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_selected(self, value: bool) -> None:
|
||||
self._selected = value
|
||||
self.selectedChanged.emit()
|
||||
@ -1,13 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtCore import Property, QObject, Signal
|
||||
from dcs.mapping import LatLng
|
||||
|
||||
from game import Game
|
||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||
from game.profiling import logged_duration
|
||||
from game.server.leaflet import LeafletLatLon
|
||||
from game.server.security import ApiKeyManager
|
||||
@ -17,7 +15,6 @@ from game.theater import (
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from .controlpointjs import ControlPointJs
|
||||
from .flightjs import FlightJs
|
||||
from .frontlinejs import FrontLineJs
|
||||
from .groundobjectjs import GroundObjectJs
|
||||
from .supplyroutejs import SupplyRouteJs
|
||||
@ -47,9 +44,8 @@ class MapModel(QObject):
|
||||
controlPointsChanged = Signal()
|
||||
groundObjectsChanged = Signal()
|
||||
supplyRoutesChanged = Signal()
|
||||
flightsChanged = Signal()
|
||||
frontLinesChanged = Signal()
|
||||
selectedFlightChanged = Signal(str)
|
||||
mapReset = Signal()
|
||||
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
@ -58,79 +54,18 @@ class MapModel(QObject):
|
||||
self._control_points = []
|
||||
self._ground_objects = []
|
||||
self._supply_routes = []
|
||||
self._flights: dict[tuple[bool, int, int], FlightJs] = {}
|
||||
self._front_lines = []
|
||||
|
||||
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
||||
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
|
||||
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
|
||||
GameUpdateSignal.get_instance().package_selection_changed.connect(
|
||||
self.set_package_selection
|
||||
)
|
||||
GameUpdateSignal.get_instance().flight_selection_changed.connect(
|
||||
self.set_flight_selection
|
||||
)
|
||||
self.reset()
|
||||
|
||||
def clear(self) -> None:
|
||||
self._control_points = []
|
||||
self._supply_routes = []
|
||||
self._ground_objects = []
|
||||
self._flights = {}
|
||||
self._front_lines = []
|
||||
self.cleared.emit()
|
||||
|
||||
def set_package_selection(self, index: int) -> None:
|
||||
self.deselect_current_flight()
|
||||
# Optional[int] isn't a valid type for a Qt signal. None will be converted to
|
||||
# zero automatically. We use -1 to indicate no selection.
|
||||
if index == -1:
|
||||
self._selected_flight_index = None
|
||||
else:
|
||||
self._selected_flight_index = index, 0
|
||||
self.select_current_flight()
|
||||
|
||||
def set_flight_selection(self, index: int) -> None:
|
||||
self.deselect_current_flight()
|
||||
if self._selected_flight_index is None:
|
||||
if index != -1:
|
||||
# We don't know what order update_package_selection and
|
||||
# update_flight_selection will be called in when the last
|
||||
# package is removed. If no flight is selected, it's not a
|
||||
# problem to also have no package selected.
|
||||
logging.error("Flight was selected with no package selected")
|
||||
return
|
||||
|
||||
# Optional[int] isn't a valid type for a Qt signal. None will be converted to
|
||||
# zero automatically. We use -1 to indicate no selection.
|
||||
if index == -1:
|
||||
self._selected_flight_index = self._selected_flight_index[0], None
|
||||
self._selected_flight_index = self._selected_flight_index[0], index
|
||||
self.select_current_flight()
|
||||
|
||||
@property
|
||||
def _selected_flight(self) -> Optional[FlightJs]:
|
||||
if self._selected_flight_index is None:
|
||||
return None
|
||||
package_index, flight_index = self._selected_flight_index
|
||||
blue = True
|
||||
return self._flights.get((blue, package_index, flight_index))
|
||||
|
||||
def deselect_current_flight(self) -> None:
|
||||
flight = self._selected_flight
|
||||
if flight is None:
|
||||
return None
|
||||
flight.set_selected(False)
|
||||
|
||||
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))
|
||||
|
||||
def reset(self) -> None:
|
||||
if self.game_model.game is None:
|
||||
self.clear()
|
||||
@ -139,8 +74,8 @@ class MapModel(QObject):
|
||||
self.reset_control_points()
|
||||
self.reset_ground_objects()
|
||||
self.reset_routes()
|
||||
self.reset_atos()
|
||||
self.reset_front_lines()
|
||||
self.mapReset.emit()
|
||||
|
||||
def on_game_load(self, game: Optional[Game]) -> None:
|
||||
if game is not None:
|
||||
@ -158,29 +93,6 @@ class MapModel(QObject):
|
||||
def mapCenter(self) -> LeafletLatLon:
|
||||
return self._map_center.as_list()
|
||||
|
||||
def _flights_in_ato(
|
||||
self, ato: AirTaskingOrder, blue: bool
|
||||
) -> dict[tuple[bool, int, int], FlightJs]:
|
||||
flights = {}
|
||||
for p_idx, package in enumerate(ato.packages):
|
||||
for f_idx, flight in enumerate(package.flights):
|
||||
flights[blue, p_idx, f_idx] = FlightJs(
|
||||
flight,
|
||||
selected=blue and (p_idx, f_idx) == self._selected_flight_index,
|
||||
ato_model=self.game_model.ato_model_for(blue),
|
||||
)
|
||||
return flights
|
||||
|
||||
def reset_atos(self) -> None:
|
||||
self._flights = self._flights_in_ato(
|
||||
self.game.blue.ato, blue=True
|
||||
) | self._flights_in_ato(self.game.red.ato, blue=False)
|
||||
self.flightsChanged.emit()
|
||||
|
||||
@Property(list, notify=flightsChanged)
|
||||
def flights(self) -> list[FlightJs]:
|
||||
return list(self._flights.values())
|
||||
|
||||
def reset_control_points(self) -> None:
|
||||
self._control_points = [
|
||||
ControlPointJs(c, self.game_model, self.game.theater)
|
||||
|
||||
@ -17,28 +17,12 @@ class GameUpdateSignal(QObject):
|
||||
|
||||
game_loaded = Signal(Game)
|
||||
|
||||
flight_paths_changed = Signal()
|
||||
package_selection_changed = Signal(int) # -1 indicates no selection.
|
||||
flight_selection_changed = Signal(int) # -1 indicates no selection.
|
||||
|
||||
def __init__(self):
|
||||
super(GameUpdateSignal, self).__init__()
|
||||
GameUpdateSignal.instance = self
|
||||
|
||||
self.game_loaded.connect(self.updateGame)
|
||||
|
||||
def select_package(self, index: Optional[int]) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_selection_changed.emit(-1 if index is None else index)
|
||||
|
||||
def select_flight(self, index: Optional[int]) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_selection_changed.emit(-1 if index is None else index)
|
||||
|
||||
def redraw_flight_paths(self) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_paths_changed.emit()
|
||||
|
||||
def updateGame(self, game: Optional[Game]):
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.gameupdated.emit(game)
|
||||
|
||||
@ -5,9 +5,10 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
|
||||
from game.ato.flight import Flight
|
||||
from game.server import EventStream
|
||||
from game.sim import GameUpdateEvents
|
||||
from qt_ui.models import GameModel, PackageModel
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
||||
|
||||
|
||||
@ -24,6 +25,7 @@ class QEditFlightDialog(QDialog):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.game_model = game_model
|
||||
self.flight = flight
|
||||
|
||||
self.setWindowTitle("Edit flight")
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
@ -37,5 +39,5 @@ class QEditFlightDialog(QDialog):
|
||||
self.finished.connect(self.on_close)
|
||||
|
||||
def on_close(self, _result) -> None:
|
||||
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||
EventStream.put_nowait(GameUpdateEvents().update_flight(self.flight))
|
||||
self.game_model.ato_model.client_slots_changed.emit()
|
||||
|
||||
@ -16,14 +16,15 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.flightplan import FlightPlanBuilder, PlanningError
|
||||
from game.ato.package import Package
|
||||
from game.game import Game
|
||||
from game.server import EventStream
|
||||
from game.sim import GameUpdateEvents
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.ato.flightplan import FlightPlanBuilder, PlanningError
|
||||
from qt_ui.models import AtoModel, GameModel, PackageModel
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.widgets.ato import QFlightList
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
|
||||
|
||||
|
||||
@ -141,9 +142,10 @@ class QPackageDialog(QDialog):
|
||||
def on_cancel(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def on_close(_result) -> None:
|
||||
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||
def on_close(self, _result) -> None:
|
||||
EventStream.put_nowait(
|
||||
GameUpdateEvents().update_flights_in_package(self.package_model.package)
|
||||
)
|
||||
|
||||
def on_save(self) -> None:
|
||||
self.save_tot()
|
||||
@ -183,13 +185,14 @@ class QPackageDialog(QDialog):
|
||||
)
|
||||
try:
|
||||
planner.populate_flight_plan(flight)
|
||||
self.package_model.update_tot()
|
||||
EventStream.put_nowait(GameUpdateEvents().new_flight(flight))
|
||||
except PlanningError as ex:
|
||||
self.package_model.delete_flight(flight)
|
||||
logging.exception("Could not create flight")
|
||||
QMessageBox.critical(
|
||||
self, "Could not create flight", str(ex), QMessageBox.Ok
|
||||
)
|
||||
self.package_model.update_tot()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_changed.emit()
|
||||
|
||||
@ -252,7 +255,7 @@ class QNewPackageDialog(QPackageDialog):
|
||||
def on_cancel(self) -> None:
|
||||
super().on_cancel()
|
||||
for flight in self.package_model.package.flights:
|
||||
flight.return_pilots_and_aircraft()
|
||||
self.package_model.delete_flight(flight)
|
||||
|
||||
|
||||
class QEditPackageDialog(QPackageDialog):
|
||||
|
||||
@ -382,48 +382,61 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||
game.groundObjectsChanged.connect(drawGroundObjects);
|
||||
game.supplyRoutesChanged.connect(drawSupplyRoutes);
|
||||
game.frontLinesChanged.connect(drawFrontLines);
|
||||
game.flightsChanged.connect(drawAircraft);
|
||||
game.selectedFlightChanged.connect(updateSelectedFlight);
|
||||
game.mapReset.connect(drawAircraft);
|
||||
});
|
||||
|
||||
function handleStreamedEvents(events) {
|
||||
for (const [flightId, position] of Object.entries(events.updated_flights)) {
|
||||
Flight.withId(flightId).drawAircraftLocation(position);
|
||||
for (const [flightId, position] of Object.entries(
|
||||
events.updated_flight_positions
|
||||
)) {
|
||||
Flight.withId(flightId).updatePosition(position);
|
||||
}
|
||||
|
||||
for (const combat of events.new_combats) {
|
||||
redrawCombat(combat);
|
||||
}
|
||||
|
||||
for (const combat of events.updated_combats) {
|
||||
redrawCombat(combat);
|
||||
}
|
||||
|
||||
for (const player of events.navmesh_updates) {
|
||||
drawNavmesh(player);
|
||||
}
|
||||
|
||||
if (events.unculled_zones_updated) {
|
||||
drawUnculledZones();
|
||||
}
|
||||
|
||||
if (events.threat_zones_updated) {
|
||||
drawThreatZones();
|
||||
}
|
||||
|
||||
if (events.deselected_flight && Flight.selected != null) {
|
||||
Flight.deselectCurrent();
|
||||
}
|
||||
|
||||
if (events.selected_flight != null) {
|
||||
Flight.select(events.selected_flight);
|
||||
}
|
||||
|
||||
for (const flight of events.new_flights) {
|
||||
new Flight(flight).draw();
|
||||
}
|
||||
|
||||
for (const flightId of events.updated_flights) {
|
||||
Flight.withId(flightId).draw();
|
||||
}
|
||||
|
||||
for (const flightId of events.deleted_flights) {
|
||||
Flight.popId(flightId).clear();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@ -857,15 +870,17 @@ class Waypoint {
|
||||
|
||||
class Flight {
|
||||
static registeredFlights = {};
|
||||
static selected = null;
|
||||
|
||||
constructor(flight) {
|
||||
this.flight = flight;
|
||||
this.id = flight.id;
|
||||
this.selected = false;
|
||||
self.position = flight.position;
|
||||
this.aircraft = null;
|
||||
this.path = null;
|
||||
this.markers = [];
|
||||
this.commitBoundary = null;
|
||||
this.flight.selectedChanged.connect(() => this.draw());
|
||||
Flight.registerFlight(this);
|
||||
}
|
||||
|
||||
@ -877,18 +892,67 @@ class Flight {
|
||||
Flight.registeredFlights[flight.id] = flight;
|
||||
}
|
||||
|
||||
static unregisterFlight(id) {
|
||||
if (Flight.selected != null && Flight.selected.id == id) {
|
||||
Flight.clearSelected();
|
||||
}
|
||||
delete Flight.registeredFlights[id];
|
||||
}
|
||||
|
||||
static withId(id) {
|
||||
return Flight.registeredFlights[id];
|
||||
}
|
||||
|
||||
static popId(id) {
|
||||
const flight = Flight.withId(id);
|
||||
Flight.unregisterFlight(id);
|
||||
return flight;
|
||||
}
|
||||
|
||||
static clearSelected() {
|
||||
holdZones.clearLayers();
|
||||
ipZones.clearLayers();
|
||||
joinZones.clearLayers();
|
||||
Flight.selected = null;
|
||||
}
|
||||
|
||||
static deselectCurrent() {
|
||||
const flight = Flight.selected;
|
||||
Flight.clearSelected();
|
||||
if (flight != null) {
|
||||
flight.selected = false;
|
||||
flight.draw();
|
||||
}
|
||||
}
|
||||
|
||||
static select(id) {
|
||||
if (Flight.selected != null && Flight.selected.id == id) {
|
||||
return;
|
||||
}
|
||||
|
||||
Flight.deselectCurrent();
|
||||
const flight = Flight.withId(id);
|
||||
Flight.selected = flight;
|
||||
flight.selected = true;
|
||||
flight.draw();
|
||||
drawHoldZones(id);
|
||||
drawIpZones(id);
|
||||
drawJoinZones(id);
|
||||
}
|
||||
|
||||
shouldMark(waypoint) {
|
||||
return this.flight.selected && waypoint.shouldMark();
|
||||
return this.selected && waypoint.shouldMark();
|
||||
}
|
||||
|
||||
flightPlanLayer() {
|
||||
return this.flight.blue ? blueFlightPlansLayer : redFlightPlansLayer;
|
||||
}
|
||||
|
||||
updatePosition(position) {
|
||||
this.position = position;
|
||||
this.drawAircraftLocation();
|
||||
}
|
||||
|
||||
updatePath(idx, position) {
|
||||
const points = this.path.getLatLngs();
|
||||
points[idx] = position;
|
||||
@ -898,7 +962,7 @@ class Flight {
|
||||
drawPath(path) {
|
||||
const color = this.flight.blue ? Colors.Blue : Colors.Red;
|
||||
const layer = this.flightPlanLayer();
|
||||
if (this.flight.selected) {
|
||||
if (this.selected) {
|
||||
this.path = L.polyline(path, {
|
||||
color: Colors.Highlight,
|
||||
interactive: false,
|
||||
@ -919,46 +983,69 @@ class Flight {
|
||||
this.drawCommitBoundary();
|
||||
}
|
||||
|
||||
drawAircraftLocation(position = null) {
|
||||
clear() {
|
||||
this.clearAircraftLocation();
|
||||
this.clearFlightPlan();
|
||||
this.clearCommitBoundary();
|
||||
}
|
||||
|
||||
clearAircraftLocation() {
|
||||
if (this.aircraft != null) {
|
||||
this.aircraft.removeFrom(aircraftLayer);
|
||||
this.aircraft = null;
|
||||
}
|
||||
if (position == null) {
|
||||
position = this.flight.position;
|
||||
}
|
||||
if (position.length > 0) {
|
||||
this.aircraft = L.marker(position, {
|
||||
icon: Icons.AirIcons.icon(
|
||||
"fighter",
|
||||
this.flight.blue,
|
||||
this.flight.selected
|
||||
),
|
||||
}
|
||||
|
||||
drawAircraftLocation() {
|
||||
this.clearAircraftLocation();
|
||||
if (this.position != null) {
|
||||
this.aircraft = L.marker(this.position, {
|
||||
icon: Icons.AirIcons.icon("fighter", this.flight.blue, this.selected),
|
||||
}).addTo(aircraftLayer);
|
||||
}
|
||||
}
|
||||
|
||||
drawCommitBoundary() {
|
||||
clearCommitBoundary() {
|
||||
if (this.commitBoundary != null) {
|
||||
this.commitBoundary
|
||||
.removeFrom(selectedFlightPlansLayer)
|
||||
.removeFrom(this.flightPlanLayer())
|
||||
.removeFrom(allFlightPlansLayer);
|
||||
}
|
||||
if (this.flight.selected) {
|
||||
getJson(`/flights/${this.flight.id}/commit-boundary`).then((boundary) => {
|
||||
if (boundary) {
|
||||
this.commitBoundary = L.polyline(boundary, {
|
||||
color: Colors.Highlight,
|
||||
weight: 1,
|
||||
interactive: false,
|
||||
})
|
||||
.addTo(selectedFlightPlansLayer)
|
||||
.addTo(this.flightPlanLayer())
|
||||
.addTo(allFlightPlansLayer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
drawCommitBoundary() {
|
||||
if (!this.selected) {
|
||||
this.clearCommitBoundary();
|
||||
return;
|
||||
}
|
||||
|
||||
getJson(`/flights/${this.flight.id}/commit-boundary`).then((boundary) => {
|
||||
// For a selected flight we wait to clear the commit boundary until after
|
||||
// the backend responds. Otherwise if the package is reselected while
|
||||
// waiting we may have the following execution order, with selections A
|
||||
// and B:
|
||||
//
|
||||
// 1. A: clear
|
||||
// 2. A: wait for backend
|
||||
// 3. B: wait for backend
|
||||
// 4. A: Add boundary to map
|
||||
// 5. B: Add boundary to map
|
||||
//
|
||||
// Similarly, we need to recheck that we're still selected before
|
||||
// continuing.
|
||||
this.clearCommitBoundary();
|
||||
if (boundary && this.selected) {
|
||||
this.commitBoundary = L.polyline(boundary, {
|
||||
color: Colors.Highlight,
|
||||
weight: 1,
|
||||
interactive: false,
|
||||
})
|
||||
.addTo(selectedFlightPlansLayer)
|
||||
.addTo(this.flightPlanLayer())
|
||||
.addTo(allFlightPlansLayer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearFlightPlan() {
|
||||
@ -978,39 +1065,23 @@ class Flight {
|
||||
}
|
||||
|
||||
drawFlightPlan() {
|
||||
this.clearFlightPlan();
|
||||
this.flight.flightIsInAto().then((inAto) => {
|
||||
if (!inAto) {
|
||||
// HACK: The signal to redraw the ATO following package/flight deletion
|
||||
// and the signal to change flight/package selection due to UI selection
|
||||
// change come in an arbitrary order. If redraw signal comes first the
|
||||
// UI will clear the map and redraw, but then when the UI updates
|
||||
// selection away from the (now deleted) flight/package it calls
|
||||
// deselect, which redraws the deleted flight plan in its deselected
|
||||
// state.
|
||||
//
|
||||
// Avoid this by checking that the flight is still in the coalition's
|
||||
// ATO before drawing.
|
||||
return;
|
||||
}
|
||||
|
||||
getJson(`/waypoints/${this.flight.id}`).then((waypoints) => {
|
||||
const path = [];
|
||||
waypoints.map((raw, idx) => {
|
||||
const waypoint = new Waypoint(raw, idx, this);
|
||||
if (waypoint.includeInPath()) {
|
||||
path.push(waypoint.position());
|
||||
}
|
||||
if (this.shouldMark(waypoint)) {
|
||||
waypoint.marker
|
||||
.addTo(selectedFlightPlansLayer)
|
||||
.addTo(this.flightPlanLayer())
|
||||
.addTo(allFlightPlansLayer);
|
||||
this.markers.push(waypoint.marker);
|
||||
}
|
||||
});
|
||||
this.drawPath(path);
|
||||
getJson(`/waypoints/${this.flight.id}`).then((waypoints) => {
|
||||
this.clearFlightPlan();
|
||||
const path = [];
|
||||
waypoints.map((raw, idx) => {
|
||||
const waypoint = new Waypoint(raw, idx, this);
|
||||
if (waypoint.includeInPath()) {
|
||||
path.push(waypoint.position());
|
||||
}
|
||||
if (this.shouldMark(waypoint)) {
|
||||
waypoint.marker
|
||||
.addTo(selectedFlightPlansLayer)
|
||||
.addTo(this.flightPlanLayer())
|
||||
.addTo(allFlightPlansLayer);
|
||||
this.markers.push(waypoint.marker);
|
||||
}
|
||||
});
|
||||
this.drawPath(path);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1022,22 +1093,12 @@ function drawAircraft() {
|
||||
redFlightPlansLayer.clearLayers();
|
||||
selectedFlightPlansLayer.clearLayers();
|
||||
allFlightPlansLayer.clearLayers();
|
||||
let selected = null;
|
||||
game.flights.forEach((flight) => {
|
||||
// Draw the selected waypoint last so it's on top. bringToFront only brings
|
||||
// it to the front of the *extant* elements, so any flights drawn later will
|
||||
// be drawn on top. We could fight with manual Z-indexes but leaflet does a
|
||||
// lot of that automatically so it'd be error prone.
|
||||
if (flight.selected) {
|
||||
selected = flight;
|
||||
} else {
|
||||
|
||||
getJson("/flights").then((flights) => {
|
||||
for (const flight of flights) {
|
||||
new Flight(flight).draw();
|
||||
}
|
||||
});
|
||||
|
||||
if (selected != null) {
|
||||
new Flight(selected).draw();
|
||||
}
|
||||
}
|
||||
|
||||
function _drawThreatZones(zones, layer, player) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user