diff --git a/client/src/api/_liberationApi.ts b/client/src/api/_liberationApi.ts index 9e321ec4..4aba5980 100644 --- a/client/src/api/_liberationApi.ts +++ b/client/src/api/_liberationApi.ts @@ -479,6 +479,7 @@ export type Game = { threat_zones: ThreatZoneContainer; navmeshes: NavMeshes; map_center?: LatLng; + unculled_zones: UnculledZone[]; }; export type MapZones = { inclusion: LatLng[][]; diff --git a/client/src/api/eventstream.tsx b/client/src/api/eventstream.tsx index 3ca961e4..b259e61e 100644 --- a/client/src/api/eventstream.tsx +++ b/client/src/api/eventstream.tsx @@ -28,6 +28,7 @@ import { import { navMeshUpdated } from "./navMeshSlice"; import { updateTgo } from "./tgosSlice"; import { threatZonesUpdated } from "./threatZonesSlice"; +import { unculledZonesUpdated } from "./unculledZonesSlice"; import { LatLng } from "leaflet"; import { updateIadsConnection } from "./iadsNetworkSlice"; import { IadsConnection } from "./_liberationApi"; @@ -87,6 +88,16 @@ export const handleStreamedEvents = ( }); } + if (events.unculled_zones_updated) { + backend.get(`/map-zones/unculled`).then( + (result) => { + if (result.data) { + dispatch(unculledZonesUpdated(result.data)); + } + } + ); + } + if (events.threat_zones_updated) { dispatch(liberationApi.endpoints.getThreatZones.initiate()).then( (result) => { diff --git a/client/src/api/unculledZonesSlice.ts b/client/src/api/unculledZonesSlice.ts new file mode 100644 index 00000000..6ffd37c9 --- /dev/null +++ b/client/src/api/unculledZonesSlice.ts @@ -0,0 +1,36 @@ +import { RootState } from "../app/store"; +import { gameLoaded, gameUnloaded } from "./actions"; +import { UnculledZone } from "./liberationApi"; +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; + +interface UnculledZonesState { + zones: UnculledZone[]; +} + +const initialState: UnculledZonesState = { + zones: [], +}; + +export const unculledZonesSlice = createSlice({ + name: "unculledZonesState", + initialState, + reducers: { + updated: (state, action: PayloadAction) => { + state.zones = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(gameLoaded, (state, action) => { + state.zones = action.payload.unculled_zones; + }); + builder.addCase(gameUnloaded, (state) => { + state.zones = initialState.zones; + }); + }, +}); + +export const { updated: unculledZonesUpdated } = unculledZonesSlice.actions; + +export const selectUnculledZones = (state: RootState) => state.unculledZones; + +export default unculledZonesSlice.reducer; diff --git a/client/src/app/store.ts b/client/src/app/store.ts index 43c4b47c..46fcc228 100644 --- a/client/src/app/store.ts +++ b/client/src/app/store.ts @@ -9,6 +9,7 @@ import supplyRoutesReducer from "../api/supplyRoutesSlice"; import tgosReducer from "../api/tgosSlice"; import iadsNetworkReducer from "../api/iadsNetworkSlice"; import threatZonesReducer from "../api/threatZonesSlice"; +import unculledZonesReducer from "../api/unculledZonesSlice"; import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit"; export const store = configureStore({ @@ -24,6 +25,7 @@ export const store = configureStore({ tgos: tgosReducer, threatZones: threatZonesReducer, [baseApi.reducerPath]: baseApi.reducer, + unculledZones: unculledZonesReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(baseApi.middleware), diff --git a/client/src/components/cullingexclusionzones/CullingExclusionZones.tsx b/client/src/components/cullingexclusionzones/CullingExclusionZones.tsx new file mode 100644 index 00000000..8e8bc12d --- /dev/null +++ b/client/src/components/cullingexclusionzones/CullingExclusionZones.tsx @@ -0,0 +1,47 @@ +import { UnculledZone } from "../../api/liberationApi"; +import { selectUnculledZones } from "../../api/unculledZonesSlice"; +import { useAppSelector } from "../../app/hooks"; +import { LayerGroup, LayersControl, Circle } from "react-leaflet"; + +interface CullingExclusionCirclesProps { + zones: UnculledZone[]; +} + +const CullingExclusionCircles = (props: CullingExclusionCirclesProps) => { + return ( + <> + + {props.zones.map((zone, idx) => { + return ( + + ); + })} + + + ); +}; + +export default function CullingExclusionZones() { + const data = useAppSelector(selectUnculledZones).zones; + var cez = <>; + + if (!data) { + console.log("Empty response when loading culling exclusion zones"); + } else { + cez = ( + + ); + } + return ( + + {cez} + + ); +} diff --git a/client/src/components/cullingexclusionzones/index.ts b/client/src/components/cullingexclusionzones/index.ts new file mode 100644 index 00000000..5c74e33e --- /dev/null +++ b/client/src/components/cullingexclusionzones/index.ts @@ -0,0 +1 @@ +export { default } from "./CullingExclusionZones"; diff --git a/client/src/components/liberationmap/LiberationMap.tsx b/client/src/components/liberationmap/LiberationMap.tsx index 5863184c..1f69f44b 100644 --- a/client/src/components/liberationmap/LiberationMap.tsx +++ b/client/src/components/liberationmap/LiberationMap.tsx @@ -18,6 +18,7 @@ import { useEffect, useRef } from "react"; import { BasemapLayer } from "react-esri-leaflet"; import { LayersControl, MapContainer, ScaleControl } from "react-leaflet"; import Iadsnetworklayer from "../iadsnetworklayer"; +import CullingExclusionZones from "../cullingexclusionzones/CullingExclusionZones" import LeafletRuler from "../ruler/Ruler"; export default function LiberationMap() { @@ -109,6 +110,7 @@ export default function LiberationMap() { + diff --git a/game/game.py b/game/game.py index 6e88cf96..a477e842 100644 --- a/game/game.py +++ b/game/game.py @@ -217,7 +217,7 @@ class Game: naming.namegen = self.name_generator LuaPluginManager.load_settings(self.settings) ObjectiveDistanceCache.set_theater(self.theater) - self.compute_unculled_zones() + self.compute_unculled_zones(GameUpdateEvents()) if not game_still_initializing: # We don't need to push events that happen during load. The UI will fully # reset when we're done. @@ -417,7 +417,7 @@ class Game: # Update cull zones with logged_duration("Computing culling positions"): - self.compute_unculled_zones() + self.compute_unculled_zones(events) def message(self, title: str, text: str = "") -> None: self.informations.append(Information(title, text, turn=self.turn)) @@ -459,7 +459,7 @@ class Game: def navmesh_for(self, player: bool) -> NavMesh: return self.coalition_for(player).nav_mesh - def compute_unculled_zones(self) -> None: + def compute_unculled_zones(self, events: GameUpdateEvents) -> None: """ Compute the current conflict position(s) used for culling calculation """ @@ -514,6 +514,7 @@ class Game: zones.append(package.target.position) self.__culling_zones = zones + events.update_unculled_zones() def add_destroyed_units(self, data: dict[str, Union[float, str]]) -> None: pos = Point( diff --git a/game/server/game/models.py b/game/server/game/models.py index 12f2534e..b5089d2e 100644 --- a/game/server/game/models.py +++ b/game/server/game/models.py @@ -8,7 +8,7 @@ from game.server.controlpoints.models import ControlPointJs from game.server.flights.models import FlightJs from game.server.frontlines.models import FrontLineJs from game.server.leaflet import LeafletPoint -from game.server.mapzones.models import ThreatZoneContainerJs +from game.server.mapzones.models import ThreatZoneContainerJs, UnculledZoneJs from game.server.navmesh.models import NavMeshesJs from game.server.supplyroutes.models import SupplyRouteJs from game.server.tgos.models import TgoJs @@ -28,6 +28,7 @@ class GameJs(BaseModel): threat_zones: ThreatZoneContainerJs navmeshes: NavMeshesJs map_center: LeafletPoint | None + unculled_zones: list[UnculledZoneJs] class Config: title = "Game" @@ -44,4 +45,5 @@ class GameJs(BaseModel): threat_zones=ThreatZoneContainerJs.for_game(game), navmeshes=NavMeshesJs.from_game(game), map_center=game.theater.terrain.map_view_default.position.latlng(), + unculled_zones=UnculledZoneJs.from_game(game), ) diff --git a/game/server/mapzones/models.py b/game/server/mapzones/models.py index 9b9530d6..d253be50 100644 --- a/game/server/mapzones/models.py +++ b/game/server/mapzones/models.py @@ -28,6 +28,16 @@ class UnculledZoneJs(BaseModel): class Config: title = "UnculledZone" + @staticmethod + def from_game(game: Game) -> list[UnculledZoneJs]: + return [ + UnculledZoneJs( + position=zone.latlng(), + radius=game.settings.perf_culling_distance * 1000, + ) + for zone in game.get_culling_zones() + ] + class ThreatZonesJs(BaseModel): full: list[LeafletPoly] diff --git a/game/server/mapzones/routes.py b/game/server/mapzones/routes.py index 1ed805b2..83dc9978 100644 --- a/game/server/mapzones/routes.py +++ b/game/server/mapzones/routes.py @@ -27,12 +27,7 @@ def get_terrain(game: Game = Depends(GameContext.require)) -> MapZonesJs: def get_unculled_zones( game: Game = Depends(GameContext.require), ) -> list[UnculledZoneJs]: - return [ - UnculledZoneJs( - position=zone.latlng(), radius=game.settings.perf_culling_distance * 1000 - ) - for zone in game.get_culling_zones() - ] + return UnculledZoneJs.from_game(game) @router.get( diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index f013529d..817b54d1 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -22,6 +22,7 @@ from PySide2.QtWidgets import ( import qt_ui.uiconstants as CONST from game.game import Game +from game.server import EventStream from game.settings import ( BooleanOption, BoundedFloatOption, @@ -31,6 +32,7 @@ from game.settings import ( OptionDescription, Settings, ) +from game.sim import GameUpdateEvents from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.spinsliders import FloatSpinSlider, TimeInputs from qt_ui.windows.GameUpdateSignal import GameUpdateSignal @@ -356,7 +358,9 @@ class QSettingsWindow(QDialog): self.cheat_options.show_base_capture_cheat ) - self.game.compute_unculled_zones() + events = GameUpdateEvents() + self.game.compute_unculled_zones(events) + EventStream.put_nowait(events) GameUpdateSignal.get_instance().updateGame(self.game) def onSelectionChanged(self):