Move TGOs out of MapModel.

This commit is contained in:
Dan Albert 2022-03-03 17:10:12 -08:00
parent d0ad554e14
commit c5c596dc2f
19 changed files with 186 additions and 186 deletions

View File

@ -4,10 +4,11 @@ from .database import Database
if TYPE_CHECKING:
from game.ato import Flight
from game.theater import FrontLine
from game.theater import FrontLine, TheaterGroundObject
class GameDb:
def __init__(self) -> None:
self.flights: Database[Flight] = Database()
self.front_lines: Database[FrontLine] = Database()
self.tgos: Database[TheaterGroundObject] = Database()

View File

@ -278,6 +278,8 @@ class Game:
for control_point in self.theater.controlpoints:
control_point.initialize_turn_0()
for tgo in control_point.connected_objectives:
self.db.tgos.add(tgo.id, tgo)
self.blue.preinit_turn_0()
self.red.preinit_turn_0()

View File

@ -9,7 +9,7 @@ from . import (
frontlines,
mapzones,
navmesh,
packagedialog,
qt,
supplyroutes,
tgos,
waypoints,
@ -29,7 +29,7 @@ app.include_router(flights.router)
app.include_router(frontlines.router)
app.include_router(mapzones.router)
app.include_router(navmesh.router)
app.include_router(packagedialog.router)
app.include_router(qt.router)
app.include_router(supplyroutes.router)
app.include_router(tgos.router)
app.include_router(waypoints.router)

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Callable, TYPE_CHECKING
from game.theater import MissionTarget
from game.theater import MissionTarget, TheaterGroundObject
if TYPE_CHECKING:
from game import Game
@ -28,8 +28,13 @@ class GameContext:
class QtCallbacks:
def __init__(self, create_new_package: Callable[[MissionTarget], None]) -> None:
def __init__(
self,
create_new_package: Callable[[MissionTarget], None],
show_tgo_info: Callable[[TheaterGroundObject], None],
) -> None:
self.create_new_package = create_new_package
self.show_tgo_info = show_tgo_info
class QtContext:

View File

@ -31,6 +31,7 @@ class GameUpdateEventsJs(BaseModel):
new_front_lines: list[FrontLineJs]
updated_front_lines: set[UUID]
deleted_front_lines: set[UUID]
updated_tgos: set[UUID]
@classmethod
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
@ -62,4 +63,5 @@ class GameUpdateEventsJs(BaseModel):
],
updated_front_lines=events.updated_front_lines,
deleted_front_lines=events.deleted_front_lines,
updated_tgos=events.updated_tgos,
)

View File

@ -1,18 +0,0 @@
from uuid import UUID
from fastapi import APIRouter, Depends
from game import Game
from ..dependencies import GameContext, QtCallbacks, QtContext
router: APIRouter = APIRouter(prefix="/package-dialog")
@router.post("/front-line/{front_line_id}")
def new_front_line_package(
front_line_id: UUID,
game: Game = Depends(GameContext.get),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
front_line = game.db.front_lines.get(front_line_id)
qt.create_new_package(front_line)

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

@ -0,0 +1,35 @@
from uuid import UUID
from fastapi import APIRouter, Depends
from game import Game
from ..dependencies import GameContext, QtCallbacks, QtContext
router: APIRouter = APIRouter(prefix="/qt")
@router.post("/create-package/front-line/{front_line_id}")
def new_front_line_package(
front_line_id: UUID,
game: Game = Depends(GameContext.get),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
qt.create_new_package(game.db.front_lines.get(front_line_id))
@router.post("/create-package/tgo/{tgo_id}")
def new_tgo_package(
tgo_id: UUID,
game: Game = Depends(GameContext.get),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
qt.create_new_package(game.db.tgos.get(tgo_id))
@router.post("/info/tgo/{tgo_id}")
def show_tgo_info(
tgo_id: UUID,
game: Game = Depends(GameContext.get),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
qt.show_tgo_info(game.db.tgos.get(tgo_id))

View File

@ -1,5 +1,7 @@
from __future__ import annotations
from uuid import UUID
from pydantic import BaseModel
from game.server.leaflet import LeafletPoint
@ -7,16 +9,17 @@ from game.theater import TheaterGroundObject
class TgoJs(BaseModel):
id: UUID
name: str
control_point_name: str
category: str
blue: bool
position: LeafletPoint
units: list[str]
threat_ranges: list[float]
detection_ranges: list[float]
dead: bool
sidc: str
units: list[str] # TODO: Event stream
threat_ranges: list[float] # TODO: Event stream
detection_ranges: list[float] # TODO: Event stream
dead: bool # TODO: Event stream
sidc: str # TODO: Event stream
@staticmethod
def for_tgo(tgo: TheaterGroundObject) -> TgoJs:
@ -29,6 +32,7 @@ class TgoJs(BaseModel):
tgo.detection_range(group).meters for group in tgo.groups
]
return TgoJs(
id=tgo.id,
name=tgo.name,
control_point_name=tgo.control_point.name,
category=tgo.category,

View File

@ -1,3 +1,5 @@
from uuid import UUID
from fastapi import APIRouter, Depends
from game import Game
@ -15,3 +17,8 @@ def list_tgos(game: Game = Depends(GameContext.get)) -> list[TgoJs]:
if not tgo.is_control_point:
tgos.append(TgoJs.for_tgo(tgo))
return tgos
@router.get("/{tgo_id}")
def get_tgo(tgo_id: UUID, game: Game = Depends(GameContext.get)) -> TgoJs:
return TgoJs.for_tgo(game.db.tgos.get(tgo_id))

View File

@ -9,7 +9,7 @@ from dcs import Point
if TYPE_CHECKING:
from game.ato import Flight, Package
from game.sim.combat import FrozenCombat
from game.theater import FrontLine
from game.theater import FrontLine, TheaterGroundObject
@dataclass
@ -30,6 +30,7 @@ class GameUpdateEvents:
new_front_lines: set[FrontLine] = field(default_factory=set)
updated_front_lines: set[UUID] = field(default_factory=set)
deleted_front_lines: set[UUID] = field(default_factory=set)
updated_tgos: set[UUID] = field(default_factory=set)
shutting_down: bool = False
@property
@ -111,6 +112,10 @@ class GameUpdateEvents:
self.deleted_front_lines.add(front_line.id)
return self
def update_tgo(self, tgo: TheaterGroundObject) -> GameUpdateEvents:
self.updated_tgos.add(tgo.id)
return self
def shut_down(self) -> GameUpdateEvents:
self.shutting_down = True
return self

View File

@ -30,7 +30,7 @@ class MissionResultsProcessor:
self.commit_convoy_losses(debriefing)
self.commit_cargo_ship_losses(debriefing)
self.commit_airlift_losses(debriefing)
self.commit_ground_losses(debriefing)
self.commit_ground_losses(debriefing, events)
self.commit_damaged_runways(debriefing)
self.commit_captures(debriefing, events)
self.commit_front_line_battle_impact(debriefing, events)
@ -131,11 +131,11 @@ class MissionResultsProcessor:
)
@staticmethod
def commit_ground_losses(debriefing: Debriefing) -> None:
def commit_ground_losses(debriefing: Debriefing, events: GameUpdateEvents) -> None:
for ground_object_loss in debriefing.ground_object_losses:
ground_object_loss.theater_unit.kill()
ground_object_loss.theater_unit.kill(events)
for scenery_object_loss in debriefing.scenery_object_losses:
scenery_object_loss.ground_unit.kill()
scenery_object_loss.ground_unit.kill(events)
@staticmethod
def commit_damaged_runways(debriefing: Debriefing) -> None:

View File

@ -730,11 +730,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
for squadron in self.squadrons:
self._retreat_squadron(game, squadron)
def depopulate_uncapturable_tgos(self) -> None:
def depopulate_uncapturable_tgos(self, events: GameUpdateEvents) -> None:
# TODO Rework this.
for tgo in self.connected_objectives:
if not tgo.capturable:
tgo.clear()
tgo.clear(events)
# TODO: Should be Airbase specific.
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
@ -742,7 +742,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self.ground_unit_orders.refund_all(self.coalition)
self.retreat_ground_units(game)
self.retreat_air_units(game)
self.depopulate_uncapturable_tgos()
self.depopulate_uncapturable_tgos(events)
self._coalition = new_coalition
self.base.set_strength_to_minimum()

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import itertools
import uuid
from abc import ABC
from typing import Iterator, List, Optional, TYPE_CHECKING
@ -22,6 +23,7 @@ from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
from ..utils import Distance, Heading, meters
if TYPE_CHECKING:
from game.sim import GameUpdateEvents
from .theatergroup import TheaterUnit, TheaterGroup
from .controlpoint import ControlPoint
from ..ato.flighttype import FlightType
@ -62,6 +64,7 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
sea_object: bool,
) -> None:
super().__init__(name, position)
self.id = uuid.uuid4()
self.category = category
self.heading = heading
self.control_point = control_point
@ -212,8 +215,9 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
def mark_locations(self) -> Iterator[Point]:
yield self.position
def clear(self) -> None:
def clear(self, events: GameUpdateEvents) -> None:
self.groups = []
events.update_tgo(self)
@property
def capturable(self) -> bool:

View File

@ -1,22 +1,20 @@
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Optional, Any, TYPE_CHECKING, Type
from typing import Any, Optional, TYPE_CHECKING, Type
from dcs.triggers import TriggerZone
from dcs.unittype import VehicleType, ShipType, StaticType
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unittype import UnitType
from dcs.unittype import UnitType as DcsUnitType
from game.point_with_heading import PointWithHeading
from game.utils import Heading
if TYPE_CHECKING:
from game.layout.layout import LayoutUnit, TgoLayoutGroup
from game.layout.layout import LayoutUnit
from game.sim import GameUpdateEvents
from game.theater import TheaterGroundObject
@ -58,8 +56,9 @@ class TheaterUnit:
# None for not available StaticTypes
return None
def kill(self) -> None:
def kill(self, events: GameUpdateEvents) -> None:
self.alive = False
events.update_tgo(self.ground_object)
@property
def unit_name(self) -> str:

View File

@ -1,90 +0,0 @@
from __future__ import annotations
from typing import List, Optional
from PySide2.QtCore import Property, QObject, Signal, Slot
from game import Game
from game.server.leaflet import LeafletLatLon
from game.theater import TheaterGroundObject
from qt_ui.dialogs import Dialog
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
class GroundObjectJs(QObject):
nameChanged = Signal()
controlPointNameChanged = Signal()
sidcChanged = Signal()
unitsChanged = Signal()
blueChanged = Signal()
positionChanged = Signal()
samThreatRangesChanged = Signal()
samDetectionRangesChanged = Signal()
categoryChanged = Signal()
deadChanged = Signal()
def __init__(self, tgo: TheaterGroundObject, game: Game) -> None:
super().__init__()
self.tgo = tgo
self.game = game
self.theater = game.theater
self.dialog: Optional[QGroundObjectMenu] = None
@Slot()
def showInfoDialog(self) -> None:
if self.dialog is None:
self.dialog = QGroundObjectMenu(
None,
self.tgo,
self.tgo.control_point,
self.game,
)
self.dialog.show()
@Slot()
def showPackageDialog(self) -> None:
Dialog.open_new_package_dialog(self.tgo)
@Property(str, notify=nameChanged)
def name(self) -> str:
return self.tgo.name
@Property(str, notify=controlPointNameChanged)
def controlPointName(self) -> str:
return self.tgo.control_point.name
@Property(str, notify=sidcChanged)
def sidc(self) -> str:
return str(self.tgo.sidc())
@Property(str, notify=categoryChanged)
def category(self) -> str:
return self.tgo.category
@Property(list, notify=unitsChanged)
def units(self) -> List[str]:
return [unit.display_name for unit in self.tgo.units]
@Property(bool, notify=blueChanged)
def blue(self) -> bool:
return self.tgo.control_point.captured
@Property(list, notify=positionChanged)
def position(self) -> LeafletLatLon:
return self.tgo.position.latlng().as_list()
@Property(bool, notify=deadChanged)
def dead(self) -> bool:
return not any(g.alive_units > 0 for g in self.tgo.groups)
@Property(list, notify=samThreatRangesChanged)
def samThreatRanges(self) -> List[float]:
if not self.tgo.might_have_aa:
return []
return [self.tgo.threat_range(group).meters for group in self.tgo.groups]
@Property(list, notify=samDetectionRangesChanged)
def samDetectionRanges(self) -> List[float]:
if not self.tgo.might_have_aa:
return []
return [self.tgo.detection_range(group).meters for group in self.tgo.groups]

View File

@ -15,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 .groundobjectjs import GroundObjectJs
from .supplyroutejs import SupplyRouteJs
@ -41,7 +40,6 @@ class MapModel(QObject):
apiKeyChanged = Signal(str)
mapCenterChanged = Signal(list)
controlPointsChanged = Signal()
groundObjectsChanged = Signal()
supplyRoutesChanged = Signal()
mapReset = Signal()
@ -50,7 +48,6 @@ class MapModel(QObject):
self.game_model = game_model
self._map_center = LatLng(0, 0)
self._control_points = []
self._ground_objects = []
self._supply_routes = []
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
@ -59,7 +56,6 @@ class MapModel(QObject):
def clear(self) -> None:
self._control_points = []
self._supply_routes = []
self._ground_objects = []
self.cleared.emit()
def reset(self) -> None:
@ -68,7 +64,6 @@ class MapModel(QObject):
return
with logged_duration("Map reset"):
self.reset_control_points()
self.reset_ground_objects()
self.reset_routes()
self.mapReset.emit()
@ -99,27 +94,6 @@ class MapModel(QObject):
def controlPoints(self) -> List[ControlPointJs]:
return self._control_points
def reset_ground_objects(self) -> None:
seen = set()
self._ground_objects = []
for cp in self.game.theater.controlpoints:
for tgo in cp.ground_objects:
if tgo.name in seen:
continue
seen.add(tgo.name)
if tgo.is_control_point:
# TGOs that are the CP (CV groups) are an implementation quirk that
# we don't need to expose to the UI.
continue
self._ground_objects.append(GroundObjectJs(tgo, self.game))
self.groundObjectsChanged.emit()
@Property(list, notify=groundObjectsChanged)
def groundObjects(self) -> List[GroundObjectJs]:
return self._ground_objects
def reset_routes(self) -> None:
seen = set()
self._supply_routes = []

View File

@ -24,7 +24,7 @@ from game.layout import LAYOUTS
from game.server import EventStream, GameContext
from game.server.dependencies import QtCallbacks, QtContext
from game.server.security import ApiKeyManager
from game.theater import MissionTarget
from game.theater import MissionTarget, TheaterGroundObject
from qt_ui import liberation_install
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
@ -36,6 +36,7 @@ from qt_ui.widgets.ato import QAirTaskingOrderPanel
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from qt_ui.windows.infos.QInfoPanel import QInfoPanel
from qt_ui.windows.logs.QLogsWindow import QLogsWindow
from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard
@ -49,6 +50,7 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow
class QLiberationWindow(QMainWindow):
new_package_signal = Signal(MissionTarget)
tgo_info_signal = Signal(TheaterGroundObject)
def __init__(self, game: Optional[Game], new_map: bool) -> None:
super().__init__()
@ -63,8 +65,12 @@ class QLiberationWindow(QMainWindow):
self.new_package_signal.connect(
lambda target: Dialog.open_new_package_dialog(target, self)
)
self.tgo_info_signal.connect(self.open_tgo_info_dialog)
QtContext.set_callbacks(
QtCallbacks(lambda target: self.new_package_signal.emit(target))
QtCallbacks(
lambda target: self.new_package_signal.emit(target),
lambda tgo: self.tgo_info_signal.emit(tgo),
)
)
Dialog.set_game(self.game_model)
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
@ -437,6 +443,9 @@ class QLiberationWindow(QMainWindow):
self.debriefing = QDebriefingWindow(debrief)
self.debriefing.show()
def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show()
def _qsettings(self) -> QSettings:
return QSettings("DCS Liberation", "Qt UI")

View File

@ -253,7 +253,6 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.cleared.connect(clearAllLayers);
game.mapCenterChanged.connect(recenterMap);
game.controlPointsChanged.connect(drawControlPoints);
game.groundObjectsChanged.connect(drawGroundObjects);
game.supplyRoutesChanged.connect(drawSupplyRoutes);
game.mapReset.connect(drawAircraft);
});
@ -320,6 +319,10 @@ function handleStreamedEvents(events) {
for (const id of events.deleted_front_lines) {
FrontLine.popId(id).clear();
}
for (const id of events.updated_tgos) {
TheaterGroundObject.withId(id).update();
}
}
function recenterMap(center) {
@ -523,6 +526,39 @@ function drawControlPoints() {
class TheaterGroundObject {
constructor(tgo) {
this.tgo = tgo;
this.marker = null;
this.threatCircles = [];
this.detectionCircles = [];
TheaterGroundObject.register(this);
}
static registered = [];
static register(tgo) {
TheaterGroundObject.registered[tgo.tgo.id] = tgo;
}
static withId(id) {
return TheaterGroundObject.registered[id];
}
showInfoDialog() {
postJson(`/qt/info/tgo/${this.tgo.id}`);
}
showPackageDialog() {
postJson(`/qt/create-package/tgo/${this.tgo.id}`);
}
update() {
getJson(`/tgos/${this.tgo.id}`).then((tgo) => {
// Clear explicitly before replacing the TGO in case (though this
// shouldn't happen) the replacement data changes the layer this TGO is
// drawn on.
this.clear();
this.tgo = tgo;
this.draw();
});
}
icon() {
@ -550,27 +586,50 @@ class TheaterGroundObject {
const threatColor = this.tgo.blue ? Colors.Blue : Colors.Red;
const detectionColor = this.tgo.blue ? "#bb89ff" : "#eee17b";
this.tgo.samDetectionRanges.forEach((range) => {
this.tgo.detection_ranges.forEach((range) => {
this.detectionCircles.push(
L.circle(this.tgo.position, {
radius: range,
color: detectionColor,
fill: false,
weight: 1,
interactive: false,
}).addTo(detectionLayer);
}).addTo(detectionLayer)
);
});
this.tgo.samThreatRanges.forEach((range) => {
this.tgo.threat_ranges.forEach((range) => {
this.threatCircles.push(
L.circle(this.tgo.position, {
radius: range,
color: threatColor,
fill: false,
weight: 2,
interactive: false,
}).addTo(threatLayer);
}).addTo(threatLayer)
);
});
}
clear() {
const detectionLayer = this.tgo.blue
? blueSamDetectionLayer
: redSamDetectionLayer;
const threatLayer = this.tgo.blue ? blueSamThreatLayer : redSamThreatLayer;
if (this.marker) {
this.marker.removeFrom(this.layer());
}
for (const circle of this.threatCircles) {
circle.removeFrom(threatLayer);
}
for (const circle of this.detectionCircles) {
circle.removeFrom(detectionLayer);
}
}
draw() {
if (!this.tgo.blue && this.tgo.dead) {
// Don't bother drawing dead opfor TGOs. Blue is worth showing because
@ -579,14 +638,14 @@ class TheaterGroundObject {
return;
}
L.marker(this.tgo.position, { icon: this.icon() })
this.marker = L.marker(this.tgo.position, { icon: this.icon() })
.bindTooltip(
`${this.tgo.name} (${
this.tgo.controlPointName
this.tgo.control_point_name
})<br />${this.tgo.units.join("<br />")}`
)
.on("click", () => this.tgo.showInfoDialog())
.on("contextmenu", () => this.tgo.showPackageDialog())
.on("click", () => this.showInfoDialog())
.on("contextmenu", () => this.showPackageDialog())
.addTo(this.layer());
this.drawSamThreats();
}
@ -601,8 +660,10 @@ function drawGroundObjects() {
redSamDetectionLayer.clearLayers();
blueSamThreatLayer.clearLayers();
redSamThreatLayer.clearLayers();
game.groundObjects.forEach((tgo) => {
getJson("/tgos").then((tgos) => {
for (const tgo of tgos) {
new TheaterGroundObject(tgo).draw();
}
});
}
@ -683,7 +744,7 @@ class FrontLine {
}
openNewPackageDialog() {
postJson(`/package-dialog/front-line/${this.id}`);
postJson(`/qt/create-package/front-line/${this.id}`);
}
}