From 92236a5bc3fc12d1bb30560dfa4c8aee129233b2 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 3 Mar 2022 23:31:07 -0800 Subject: [PATCH] Update the react map for some new events. --- client/src/api/controlPointsSlice.ts | 16 ++++++--- client/src/api/eventstream.tsx | 23 ++++++++++++ client/src/api/tgo.ts | 7 ---- client/src/api/tgosSlice.ts | 36 ++++++------------- .../AirDefenseRangeLayer.tsx | 13 ++----- .../components/controlpoints/ControlPoint.tsx | 11 ++++++ .../controlpointslayer/ControlPointsLayer.tsx | 2 +- .../liberationmap/LiberationMap.tsx | 20 ++++++----- client/src/components/tgoslayer/TgosLayer.tsx | 11 +++--- game/server/controlpoints/routes.py | 15 +++++++- game/server/dependencies.py | 4 ++- game/server/eventstream/models.py | 2 ++ game/server/qt/routes.py | 32 ++++++++++++++++- game/sim/gameupdateevents.py | 7 +++- game/theater/controlpoint.py | 1 + qt_ui/windows/QLiberationWindow.py | 10 +++++- 16 files changed, 145 insertions(+), 65 deletions(-) diff --git a/client/src/api/controlPointsSlice.ts b/client/src/api/controlPointsSlice.ts index 84b823ad..c1899087 100644 --- a/client/src/api/controlPointsSlice.ts +++ b/client/src/api/controlPointsSlice.ts @@ -4,11 +4,11 @@ import { ControlPoint } from "./controlpoint"; import { RootState } from "../app/store"; interface ControlPointsState { - controlPoints: ControlPoint[]; + controlPoints: { [key: number]: ControlPoint }; } const initialState: ControlPointsState = { - controlPoints: [], + controlPoints: {}, }; export const controlPointsSlice = createSlice({ @@ -16,12 +16,20 @@ export const controlPointsSlice = createSlice({ initialState, reducers: { setControlPoints: (state, action: PayloadAction) => { - state.controlPoints = action.payload; + state.controlPoints = {}; + for (const cp of action.payload) { + state.controlPoints[cp.id] = cp; + } + }, + updateControlPoint: (state, action: PayloadAction) => { + const cp = action.payload; + state.controlPoints[cp.id] = cp; }, }, }); -export const { setControlPoints } = controlPointsSlice.actions; +export const { setControlPoints, updateControlPoint } = + controlPointsSlice.actions; export const selectControlPoints = (state: RootState) => state.controlPoints; diff --git a/client/src/api/eventstream.tsx b/client/src/api/eventstream.tsx index 41e815a9..41712642 100644 --- a/client/src/api/eventstream.tsx +++ b/client/src/api/eventstream.tsx @@ -1,8 +1,14 @@ import { deselectFlight, selectFlight } from "./flightsSlice"; import { AppDispatch } from "../app/store"; +import { ControlPoint } from "./controlpoint"; import { Flight } from "./flight"; +import FrontLine from "./frontline"; import { LatLng } from "leaflet"; +import Tgo from "./tgo"; +import backend from "./backend"; +import { updateControlPoint } from "./controlPointsSlice"; +import { updateTgo } from "./tgosSlice"; // Placeholder. We don't use this yet. This is just here so we can flesh out the // update events model. @@ -21,6 +27,11 @@ interface GameUpdateEvents { deleted_flights: string[]; selected_flight: string | null; deselected_flight: boolean; + new_front_lines: FrontLine[]; + updated_front_lines: string[]; + deleted_front_lines: string[]; + updated_tgos: string[]; + updated_control_points: number[]; } export const handleStreamedEvents = ( @@ -33,4 +44,16 @@ export const handleStreamedEvents = ( if (events.selected_flight != null) { dispatch(selectFlight(events.selected_flight)); } + for (const id of events.updated_tgos) { + backend.get(`/tgos/${id}`).then((response) => { + const tgo = response.data as Tgo; + dispatch(updateTgo(tgo)); + }); + } + for (const id of events.updated_control_points) { + backend.get(`/control-points/${id}`).then((response) => { + const cp = response.data as ControlPoint; + dispatch(updateControlPoint(cp)); + }); + } }; diff --git a/client/src/api/tgo.ts b/client/src/api/tgo.ts index cd1709d4..6f2e22b2 100644 --- a/client/src/api/tgo.ts +++ b/client/src/api/tgo.ts @@ -1,12 +1,5 @@ import { LatLng } from "leaflet"; -export enum TgoType { - AIR_DEFENSE = "Air defenses", - FACTORY = "Factories", - SHIP = "Ships", - OTHER = "Other ground objects", -} - export interface Tgo { id: string; name: string; diff --git a/client/src/api/tgosSlice.ts b/client/src/api/tgosSlice.ts index 0bc8bcc8..aa437a10 100644 --- a/client/src/api/tgosSlice.ts +++ b/client/src/api/tgosSlice.ts @@ -1,16 +1,14 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { Tgo, TgoType } from "./tgo"; import { RootState } from "../app/store"; +import { Tgo } from "./tgo"; interface TgosState { - tgosByType: { [key: string]: Tgo[] }; + tgos: { [key: string]: Tgo }; } const initialState: TgosState = { - tgosByType: Object.fromEntries( - Object.values(TgoType).map((key) => [key, []]) - ), + tgos: {}, }; export const tgosSlice = createSlice({ @@ -18,33 +16,19 @@ export const tgosSlice = createSlice({ initialState, reducers: { setTgos: (state, action: PayloadAction) => { - state.tgosByType = initialState.tgosByType; - for (const key of Object.values(TgoType)) { - state.tgosByType[key] = []; - } + state.tgos = {}; for (const tgo of action.payload) { - var type; - switch (tgo.category) { - case "aa": - type = TgoType.AIR_DEFENSE; - break; - case "factory": - type = TgoType.FACTORY; - break; - case "ship": - type = TgoType.SHIP; - break; - default: - type = TgoType.OTHER; - break; - } - state.tgosByType[type].push(tgo); + state.tgos[tgo.id] = tgo; } }, + updateTgo: (state, action: PayloadAction) => { + const tgo = action.payload; + state.tgos[tgo.id] = tgo; + }, }, }); -export const { setTgos } = tgosSlice.actions; +export const { setTgos, updateTgo } = tgosSlice.actions; export const selectTgos = (state: RootState) => state.tgos; diff --git a/client/src/components/airdefenserangelayer/AirDefenseRangeLayer.tsx b/client/src/components/airdefenserangelayer/AirDefenseRangeLayer.tsx index e6a9ba76..63244a01 100644 --- a/client/src/components/airdefenserangelayer/AirDefenseRangeLayer.tsx +++ b/client/src/components/airdefenserangelayer/AirDefenseRangeLayer.tsx @@ -49,19 +49,12 @@ interface AirDefenseRangeLayerProps { } export const AirDefenseRangeLayer = (props: AirDefenseRangeLayerProps) => { - const tgos = useAppSelector(selectTgos); - var allTgos: Tgo[] = []; - for (const tgoType of Object.values(tgos.tgosByType)) { - for (const tgo of tgoType) { - if (tgo.blue === props.blue) { - allTgos.push(tgo); - } - } - } + const tgos = Object.values(useAppSelector(selectTgos).tgos); + var tgosForSide = tgos.filter((tgo) => tgo.blue === props.blue); return ( - {allTgos.map((tgo) => { + {tgosForSide.map((tgo) => { return ( ); diff --git a/client/src/components/controlpoints/ControlPoint.tsx b/client/src/components/controlpoints/ControlPoint.tsx index d2f17382..a35800da 100644 --- a/client/src/components/controlpoints/ControlPoint.tsx +++ b/client/src/components/controlpoints/ControlPoint.tsx @@ -3,6 +3,7 @@ import { Marker, Tooltip } from "react-leaflet"; import { ControlPoint as ControlPointModel } from "../../api/controlpoint"; import { Symbol as MilSymbol } from "milsymbol"; +import backend from "../../api/backend"; function iconForControlPoint(cp: ControlPointModel) { const symbol = new MilSymbol(cp.sidc, { @@ -29,6 +30,16 @@ export default function ControlPoint(props: ControlPointProps) { // other markers are helpful so we want to keep them, but make sure the CP // is always the clickable thing. zIndexOffset={1000} + eventHandlers={{ + click: () => { + backend.post(`/qt/info/control-point/${props.controlPoint.id}`); + }, + contextmenu: () => { + backend.post( + `/qt/create-package/control-point/${props.controlPoint.id}` + ); + }, + }} >

{props.controlPoint.name}

diff --git a/client/src/components/controlpointslayer/ControlPointsLayer.tsx b/client/src/components/controlpointslayer/ControlPointsLayer.tsx index 72f4e3dc..9e2a2a7a 100644 --- a/client/src/components/controlpointslayer/ControlPointsLayer.tsx +++ b/client/src/components/controlpointslayer/ControlPointsLayer.tsx @@ -7,7 +7,7 @@ export default function ControlPointsLayer() { const controlPoints = useAppSelector(selectControlPoints); return ( - {controlPoints.controlPoints.map((controlPoint) => { + {Object.values(controlPoints.controlPoints).map((controlPoint) => { return ( ); diff --git a/client/src/components/liberationmap/LiberationMap.tsx b/client/src/components/liberationmap/LiberationMap.tsx index b378c0e5..a56af8c7 100644 --- a/client/src/components/liberationmap/LiberationMap.tsx +++ b/client/src/components/liberationmap/LiberationMap.tsx @@ -9,7 +9,6 @@ import FlightPlansLayer from "../flightplanslayer"; import FrontLinesLayer from "../frontlineslayer"; import { LatLng } from "leaflet"; import SupplyRoutesLayer from "../supplyrouteslayer"; -import { TgoType } from "../../api/tgo"; import TgosLayer from "../tgoslayer/TgosLayer"; interface GameProps { @@ -33,13 +32,18 @@ export default function LiberationMap(props: GameProps) { - {Object.values(TgoType).map((type, idx) => { - return ( - - - - ); - })} + + + + + + + + + + + + diff --git a/client/src/components/tgoslayer/TgosLayer.tsx b/client/src/components/tgoslayer/TgosLayer.tsx index be2fb24c..eff9708d 100644 --- a/client/src/components/tgoslayer/TgosLayer.tsx +++ b/client/src/components/tgoslayer/TgosLayer.tsx @@ -1,16 +1,19 @@ import { LayerGroup } from "react-leaflet"; import Tgo from "../tgos/Tgo"; -import { TgoType } from "../../api/tgo"; import { selectTgos } from "../../api/tgosSlice"; import { useAppSelector } from "../../app/hooks"; interface TgosLayerProps { - type: TgoType; + categories?: string[]; + exclude?: true; } export default function TgosLayer(props: TgosLayerProps) { - const allTgos = useAppSelector(selectTgos); - const tgos = allTgos.tgosByType[props.type]; + const allTgos = Object.values(useAppSelector(selectTgos).tgos); + const categoryFilter = props.categories ?? []; + const tgos = allTgos.filter( + (tgo) => categoryFilter.includes(tgo.category) === (props.exclude ?? false) + ); return ( {tgos.map((tgo) => { diff --git a/game/server/controlpoints/routes.py b/game/server/controlpoints/routes.py index cd5b7200..73eeecba 100644 --- a/game/server/controlpoints/routes.py +++ b/game/server/controlpoints/routes.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException, status from game import Game from .models import ControlPointJs @@ -13,3 +13,16 @@ def list_control_points(game: Game = Depends(GameContext.get)) -> list[ControlPo for control_point in game.theater.controlpoints: control_points.append(ControlPointJs.for_control_point(control_point)) return control_points + + +@router.get("/{cp_id}") +def get_control_point( + cp_id: int, game: Game = Depends(GameContext.get) +) -> ControlPointJs: + cp = game.theater.find_control_point_by_id(cp_id) + if cp is None: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + detail=f"Game has no control point with ID {cp_id}", + ) + return ControlPointJs.for_control_point(cp) diff --git a/game/server/dependencies.py b/game/server/dependencies.py index 9f8339d9..147431e3 100644 --- a/game/server/dependencies.py +++ b/game/server/dependencies.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Callable, TYPE_CHECKING -from game.theater import MissionTarget, TheaterGroundObject +from game.theater import ControlPoint, MissionTarget, TheaterGroundObject if TYPE_CHECKING: from game import Game @@ -32,9 +32,11 @@ class QtCallbacks: self, create_new_package: Callable[[MissionTarget], None], show_tgo_info: Callable[[TheaterGroundObject], None], + show_control_point_info: Callable[[ControlPoint], None], ) -> None: self.create_new_package = create_new_package self.show_tgo_info = show_tgo_info + self.show_control_point_info = show_control_point_info class QtContext: diff --git a/game/server/eventstream/models.py b/game/server/eventstream/models.py index 59e1d9a8..6bff3e9c 100644 --- a/game/server/eventstream/models.py +++ b/game/server/eventstream/models.py @@ -32,6 +32,7 @@ class GameUpdateEventsJs(BaseModel): updated_front_lines: set[UUID] deleted_front_lines: set[UUID] updated_tgos: set[UUID] + updated_control_points: set[int] @classmethod def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs: @@ -64,4 +65,5 @@ class GameUpdateEventsJs(BaseModel): updated_front_lines=events.updated_front_lines, deleted_front_lines=events.deleted_front_lines, updated_tgos=events.updated_tgos, + updated_control_points=events.updated_control_points, ) diff --git a/game/server/qt/routes.py b/game/server/qt/routes.py index 69c9ea22..f40d5781 100644 --- a/game/server/qt/routes.py +++ b/game/server/qt/routes.py @@ -1,6 +1,6 @@ from uuid import UUID -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException, status from game import Game from ..dependencies import GameContext, QtCallbacks, QtContext @@ -33,3 +33,33 @@ def show_tgo_info( qt: QtCallbacks = Depends(QtContext.get), ) -> None: qt.show_tgo_info(game.db.tgos.get(tgo_id)) + + +@router.post("/create-package/control-point/{cp_id}") +def new_cp_package( + cp_id: int, + game: Game = Depends(GameContext.get), + qt: QtCallbacks = Depends(QtContext.get), +) -> None: + cp = game.theater.find_control_point_by_id(cp_id) + if cp is None: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + detail=f"Game has no control point with ID {cp_id}", + ) + qt.create_new_package(cp) + + +@router.post("/info/control-point/{cp_id}") +def show_control_point_info( + cp_id: int, + game: Game = Depends(GameContext.get), + qt: QtCallbacks = Depends(QtContext.get), +) -> None: + cp = game.theater.find_control_point_by_id(cp_id) + if cp is None: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + detail=f"Game has no control point with ID {cp_id}", + ) + qt.show_control_point_info(cp) diff --git a/game/sim/gameupdateevents.py b/game/sim/gameupdateevents.py index abc93c88..67fd6fd8 100644 --- a/game/sim/gameupdateevents.py +++ b/game/sim/gameupdateevents.py @@ -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, TheaterGroundObject + from game.theater import ControlPoint, FrontLine, TheaterGroundObject @dataclass @@ -31,6 +31,7 @@ class GameUpdateEvents: 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) + updated_control_points: set[int] = field(default_factory=set) shutting_down: bool = False @property @@ -116,6 +117,10 @@ class GameUpdateEvents: self.updated_tgos.add(tgo.id) return self + def update_control_point(self, control_point: ControlPoint) -> GameUpdateEvents: + self.updated_control_points.add(control_point.id) + return self + def shut_down(self) -> GameUpdateEvents: self.shutting_down = True return self diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 1fdedcb4..741d5f0b 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -748,6 +748,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): self.base.set_strength_to_minimum() self._clear_front_lines(events) self._create_missing_front_lines(events) + events.update_control_point(self) @property def required_aircraft_start_type(self) -> Optional[StartType]: diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 5d7e6104..acea9df7 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -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, TheaterGroundObject +from game.theater import ControlPoint, 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.basemenu.QBaseMenu2 import QBaseMenu2 from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from qt_ui.windows.infos.QInfoPanel import QInfoPanel from qt_ui.windows.logs.QLogsWindow import QLogsWindow @@ -51,6 +52,7 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow class QLiberationWindow(QMainWindow): new_package_signal = Signal(MissionTarget) tgo_info_signal = Signal(TheaterGroundObject) + control_point_info_signal = Signal(ControlPoint) def __init__(self, game: Optional[Game], new_map: bool) -> None: super().__init__() @@ -66,10 +68,12 @@ class QLiberationWindow(QMainWindow): lambda target: Dialog.open_new_package_dialog(target, self) ) self.tgo_info_signal.connect(self.open_tgo_info_dialog) + self.control_point_info_signal.connect(self.open_control_point_info_dialog) QtContext.set_callbacks( QtCallbacks( lambda target: self.new_package_signal.emit(target), lambda tgo: self.tgo_info_signal.emit(tgo), + lambda cp: self.control_point_info_signal.emit(cp), ) ) Dialog.set_game(self.game_model) @@ -446,6 +450,10 @@ class QLiberationWindow(QMainWindow): def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None: QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show() + def open_control_point_info_dialog(self, cp: ControlPoint) -> None: + self._cp_dialog = QBaseMenu2(None, cp, self.game_model) + self._cp_dialog.show() + def _qsettings(self) -> QSettings: return QSettings("DCS Liberation", "Qt UI")