mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Handle map reset when the game is loaded/unloaded.
https://github.com/dcs-liberation/dcs_liberation/issues/2039 Partial fix for https://github.com/dcs-liberation/dcs_liberation/issues/2045 (now works in the new map, old one not fixed yet).
This commit is contained in:
parent
995e28cb32
commit
73fcfcec7b
@ -1,17 +1,14 @@
|
||||
import LiberationMap from "./components/liberationmap";
|
||||
import useEventStream from "./hooks/useEventSteam";
|
||||
import useInitialGameState from "./hooks/useInitialGameState";
|
||||
import { LatLng } from "leaflet";
|
||||
|
||||
function App() {
|
||||
const mapCenter: LatLng = new LatLng(25.58, 54.9);
|
||||
|
||||
useInitialGameState();
|
||||
useEventStream();
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<LiberationMap mapCenter={mapCenter} />
|
||||
<LiberationMap />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
5
client/src/api/actions.ts
Normal file
5
client/src/api/actions.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Game from "./game";
|
||||
import { createAction } from "@reduxjs/toolkit";
|
||||
|
||||
export const gameLoaded = createAction<Game>("game/loaded");
|
||||
export const gameUnloaded = createAction("game/unloaded");
|
||||
@ -1,4 +1,5 @@
|
||||
import { RootState } from "../app/store";
|
||||
import { gameLoaded, gameUnloaded } from "./actions";
|
||||
import { ControlPoint } from "./controlpoint";
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
@ -14,21 +15,28 @@ export const controlPointsSlice = createSlice({
|
||||
name: "controlPoints",
|
||||
initialState,
|
||||
reducers: {
|
||||
setControlPoints: (state, action: PayloadAction<ControlPoint[]>) => {
|
||||
state.controlPoints = {};
|
||||
for (const cp of action.payload) {
|
||||
state.controlPoints[cp.id] = cp;
|
||||
}
|
||||
},
|
||||
updateControlPoint: (state, action: PayloadAction<ControlPoint>) => {
|
||||
const cp = action.payload;
|
||||
state.controlPoints[cp.id] = cp;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(gameLoaded, (state, action) => {
|
||||
state.controlPoints = action.payload.control_points.reduce(
|
||||
(acc: { [key: number]: ControlPoint }, curr) => {
|
||||
acc[curr.id] = curr;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
builder.addCase(gameUnloaded, (state) => {
|
||||
state.controlPoints = {};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setControlPoints, updateControlPoint } =
|
||||
controlPointsSlice.actions;
|
||||
export const { updateControlPoint } = controlPointsSlice.actions;
|
||||
|
||||
export const selectControlPoints = (state: RootState) => state.controlPoints;
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { AppDispatch } from "../app/store";
|
||||
import { gameUnloaded } from "./actions";
|
||||
import backend from "./backend";
|
||||
import Combat from "./combat";
|
||||
import { endCombat, newCombat, updateCombat } from "./combatSlice";
|
||||
@ -19,6 +20,7 @@ import {
|
||||
updateFrontLine,
|
||||
} from "./frontLinesSlice";
|
||||
import FrontLine from "./frontline";
|
||||
import reloadGameState from "./gamestate";
|
||||
import Tgo from "./tgo";
|
||||
import { updateTgo } from "./tgosSlice";
|
||||
import { LatLng } from "leaflet";
|
||||
@ -41,6 +43,8 @@ interface GameUpdateEvents {
|
||||
deleted_front_lines: string[];
|
||||
updated_tgos: string[];
|
||||
updated_control_points: number[];
|
||||
reset_on_map_center: LatLng | null;
|
||||
game_unloaded: boolean;
|
||||
}
|
||||
|
||||
export const handleStreamedEvents = (
|
||||
@ -114,4 +118,12 @@ export const handleStreamedEvents = (
|
||||
dispatch(updateControlPoint(cp));
|
||||
});
|
||||
}
|
||||
|
||||
if (events.reset_on_map_center != null) {
|
||||
reloadGameState(dispatch);
|
||||
}
|
||||
|
||||
if (events.game_unloaded) {
|
||||
dispatch(gameUnloaded());
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { RootState } from "../app/store";
|
||||
import { gameLoaded, gameUnloaded } from "./actions";
|
||||
import { Flight } from "./flight";
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { LatLng } from "leaflet";
|
||||
@ -17,9 +18,6 @@ export const flightsSlice = createSlice({
|
||||
name: "flights",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearFlights: (state) => {
|
||||
state.flights = {};
|
||||
},
|
||||
registerFlight: (state, action: PayloadAction<Flight>) => {
|
||||
const flight = action.payload;
|
||||
if (flight.id in state.flights) {
|
||||
@ -51,10 +49,25 @@ export const flightsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(gameLoaded, (state, action) => {
|
||||
state.selected = null;
|
||||
state.flights = action.payload.flights.reduce(
|
||||
(acc: { [key: string]: Flight }, curr) => {
|
||||
acc[curr.id] = curr;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
builder.addCase(gameUnloaded, (state) => {
|
||||
state.selected = null;
|
||||
state.flights = {};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
clearFlights,
|
||||
registerFlight,
|
||||
unregisterFlight,
|
||||
updateFlight,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { RootState } from "../app/store";
|
||||
import { gameLoaded, gameUnloaded } from "./actions";
|
||||
import FrontLine from "./frontline";
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
@ -14,12 +15,6 @@ export const frontLinesSlice = createSlice({
|
||||
name: "frontLines",
|
||||
initialState,
|
||||
reducers: {
|
||||
setFrontLines: (state, action: PayloadAction<FrontLine[]>) => {
|
||||
state.fronts = {};
|
||||
for (const front of action.payload) {
|
||||
state.fronts[front.id] = front;
|
||||
}
|
||||
},
|
||||
addFrontLine: (state, action: PayloadAction<FrontLine>) => {
|
||||
const front = action.payload;
|
||||
state.fronts[front.id] = front;
|
||||
@ -32,9 +27,23 @@ export const frontLinesSlice = createSlice({
|
||||
delete state.fronts[action.payload];
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(gameLoaded, (state, action) => {
|
||||
state.fronts = action.payload.front_lines.reduce(
|
||||
(acc: { [key: string]: FrontLine }, curr) => {
|
||||
acc[curr.id] = curr;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
builder.addCase(gameUnloaded, (state) => {
|
||||
state.fronts = {};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setFrontLines, addFrontLine, updateFrontLine, deleteFrontLine } =
|
||||
export const { addFrontLine, updateFrontLine, deleteFrontLine } =
|
||||
frontLinesSlice.actions;
|
||||
|
||||
export const selectFrontLines = (state: RootState) => state.frontLines;
|
||||
|
||||
15
client/src/api/game.ts
Normal file
15
client/src/api/game.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ControlPoint } from "./controlpoint";
|
||||
import { Flight } from "./flight";
|
||||
import FrontLine from "./frontline";
|
||||
import SupplyRoute from "./supplyroute";
|
||||
import Tgo from "./tgo";
|
||||
import { LatLngLiteral } from "leaflet";
|
||||
|
||||
export default interface Game {
|
||||
control_points: ControlPoint[];
|
||||
tgos: Tgo[];
|
||||
supply_routes: SupplyRoute[];
|
||||
front_lines: FrontLine[];
|
||||
flights: Flight[];
|
||||
map_center: LatLngLiteral;
|
||||
}
|
||||
17
client/src/api/gamestate.ts
Normal file
17
client/src/api/gamestate.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { AppDispatch } from "../app/store";
|
||||
import { gameLoaded, gameUnloaded } from "./actions";
|
||||
import backend from "./backend";
|
||||
import Game from "./game";
|
||||
|
||||
export default function reloadGameState(dispatch: AppDispatch) {
|
||||
backend
|
||||
.get("/game")
|
||||
.catch((error) => console.log(`Error fetching game state: ${error}`))
|
||||
.then((response) => {
|
||||
if (response == null || response.data == null) {
|
||||
dispatch(gameUnloaded());
|
||||
return;
|
||||
}
|
||||
dispatch(gameLoaded(response.data as Game));
|
||||
});
|
||||
}
|
||||
30
client/src/api/mapSlice.ts
Normal file
30
client/src/api/mapSlice.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { RootState } from "../app/store";
|
||||
import { gameLoaded, gameUnloaded } from "./actions";
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { LatLngLiteral } from "leaflet";
|
||||
|
||||
interface MapState {
|
||||
center: LatLngLiteral;
|
||||
}
|
||||
|
||||
const initialState: MapState = {
|
||||
center: { lat: 0, lng: 0 },
|
||||
};
|
||||
|
||||
const mapSlice = createSlice({
|
||||
name: "map",
|
||||
initialState: initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(gameLoaded, (state, action) => {
|
||||
state.center = action.payload.map_center;
|
||||
});
|
||||
builder.addCase(gameUnloaded, (state) => {
|
||||
state.center = { lat: 0, lng: 0 };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const selectMapCenter = (state: RootState) => state.map.center;
|
||||
|
||||
export default mapSlice.reducer;
|
||||
@ -1,6 +1,7 @@
|
||||
import { RootState } from "../app/store";
|
||||
import { gameLoaded, gameUnloaded } from "./actions";
|
||||
import SupplyRoute from "./supplyroute";
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
interface SupplyRoutesState {
|
||||
routes: SupplyRoute[];
|
||||
@ -13,15 +14,17 @@ const initialState: SupplyRoutesState = {
|
||||
export const supplyRoutesSlice = createSlice({
|
||||
name: "supplyRoutes",
|
||||
initialState,
|
||||
reducers: {
|
||||
setSupplyRoutes: (state, action: PayloadAction<SupplyRoute[]>) => {
|
||||
state.routes = action.payload;
|
||||
},
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(gameLoaded, (state, action) => {
|
||||
state.routes = action.payload.supply_routes;
|
||||
});
|
||||
builder.addCase(gameUnloaded, (state) => {
|
||||
state.routes = [];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setSupplyRoutes } = supplyRoutesSlice.actions;
|
||||
|
||||
export const selectSupplyRoutes = (state: RootState) => state.supplyRoutes;
|
||||
|
||||
export default supplyRoutesSlice.reducer;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { RootState } from "../app/store";
|
||||
import { gameLoaded, gameUnloaded } from "./actions";
|
||||
import { Tgo } from "./tgo";
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
@ -14,20 +15,28 @@ export const tgosSlice = createSlice({
|
||||
name: "tgos",
|
||||
initialState,
|
||||
reducers: {
|
||||
setTgos: (state, action: PayloadAction<Tgo[]>) => {
|
||||
state.tgos = {};
|
||||
for (const tgo of action.payload) {
|
||||
state.tgos[tgo.id] = tgo;
|
||||
}
|
||||
},
|
||||
updateTgo: (state, action: PayloadAction<Tgo>) => {
|
||||
const tgo = action.payload;
|
||||
state.tgos[tgo.id] = tgo;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(gameLoaded, (state, action) => {
|
||||
state.tgos = action.payload.tgos.reduce(
|
||||
(acc: { [key: string]: Tgo }, curr) => {
|
||||
acc[curr.id] = curr;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
builder.addCase(gameUnloaded, (state) => {
|
||||
state.tgos = {};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setTgos, updateTgo } = tgosSlice.actions;
|
||||
export const { updateTgo } = tgosSlice.actions;
|
||||
|
||||
export const selectTgos = (state: RootState) => state.tgos;
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import combatReducer from "../api/combatSlice";
|
||||
import controlPointsReducer from "../api/controlPointsSlice";
|
||||
import flightsReducer from "../api/flightsSlice";
|
||||
import frontLinesReducer from "../api/frontLinesSlice";
|
||||
import mapReducer from "../api/mapSlice";
|
||||
import supplyRoutesReducer from "../api/supplyRoutesSlice";
|
||||
import tgosReducer from "../api/tgosSlice";
|
||||
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
|
||||
@ -13,6 +14,7 @@ export const store = configureStore({
|
||||
controlPoints: controlPointsReducer,
|
||||
flights: flightsReducer,
|
||||
frontLines: frontLinesReducer,
|
||||
map: mapReducer,
|
||||
supplyRoutes: supplyRoutesReducer,
|
||||
tgos: tgosReducer,
|
||||
[apiSlice.reducerPath]: apiSlice.reducer,
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { selectMapCenter } from "../../api/mapSlice";
|
||||
import { useAppSelector } from "../../app/hooks";
|
||||
import AircraftLayer from "../aircraftlayer";
|
||||
import AirDefenseRangeLayer from "../airdefenserangelayer";
|
||||
import CombatLayer from "../combatlayer";
|
||||
@ -7,17 +9,23 @@ import FrontLinesLayer from "../frontlineslayer";
|
||||
import SupplyRoutesLayer from "../supplyrouteslayer";
|
||||
import TgosLayer from "../tgoslayer/TgosLayer";
|
||||
import "./LiberationMap.css";
|
||||
import { LatLng } from "leaflet";
|
||||
import { Map } from "leaflet";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { BasemapLayer } from "react-esri-leaflet";
|
||||
import { LayersControl, MapContainer, ScaleControl } from "react-leaflet";
|
||||
|
||||
interface GameProps {
|
||||
mapCenter: LatLng;
|
||||
}
|
||||
|
||||
export default function LiberationMap(props: GameProps) {
|
||||
export default function LiberationMap() {
|
||||
const map = useRef<Map>();
|
||||
const mapCenter = useAppSelector(selectMapCenter);
|
||||
useEffect(() => {
|
||||
map.current?.setView(mapCenter, 8, { animate: true, duration: 1 });
|
||||
});
|
||||
return (
|
||||
<MapContainer zoom={8} center={props.mapCenter} zoomControl={false}>
|
||||
<MapContainer
|
||||
zoom={8}
|
||||
zoomControl={false}
|
||||
whenCreated={(mapInstance) => (map.current = mapInstance)}
|
||||
>
|
||||
<ScaleControl />
|
||||
<LayersControl collapsed={false}>
|
||||
<LayersControl.BaseLayer name="Imagery Clarity" checked>
|
||||
|
||||
@ -1,14 +1,4 @@
|
||||
import backend from "../api/backend";
|
||||
import { setControlPoints } from "../api/controlPointsSlice";
|
||||
import { ControlPoint } from "../api/controlpoint";
|
||||
import { Flight } from "../api/flight";
|
||||
import { registerFlight } from "../api/flightsSlice";
|
||||
import { setFrontLines } from "../api/frontLinesSlice";
|
||||
import FrontLine from "../api/frontline";
|
||||
import { setSupplyRoutes } from "../api/supplyRoutesSlice";
|
||||
import SupplyRoute from "../api/supplyroute";
|
||||
import Tgo from "../api/tgo";
|
||||
import { setTgos } from "../api/tgosSlice";
|
||||
import reloadGameState from "../api/gamestate";
|
||||
import { useAppDispatch } from "../app/hooks";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@ -19,48 +9,7 @@ import { useEffect } from "react";
|
||||
export const useInitialGameState = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
backend
|
||||
.get("/control-points")
|
||||
.catch((error) => console.log(`Error fetching control points: ${error}`))
|
||||
.then((response) => {
|
||||
if (response != null) {
|
||||
dispatch(setControlPoints(response.data as ControlPoint[]));
|
||||
}
|
||||
});
|
||||
backend
|
||||
.get("/tgos")
|
||||
.catch((error) => console.log(`Error fetching TGOs: ${error}`))
|
||||
.then((response) => {
|
||||
if (response != null) {
|
||||
dispatch(setTgos(response.data as Tgo[]));
|
||||
}
|
||||
});
|
||||
backend
|
||||
.get("/supply-routes")
|
||||
.catch((error) => console.log(`Error fetching supply routes: ${error}`))
|
||||
.then((response) => {
|
||||
if (response != null) {
|
||||
dispatch(setSupplyRoutes(response.data as SupplyRoute[]));
|
||||
}
|
||||
});
|
||||
backend
|
||||
.get("/front-lines")
|
||||
.catch((error) => console.log(`Error fetching front-lines: ${error}`))
|
||||
.then((response) => {
|
||||
if (response != null) {
|
||||
dispatch(setFrontLines(response.data as FrontLine[]));
|
||||
}
|
||||
});
|
||||
backend
|
||||
.get("/flights?with_waypoints=true")
|
||||
.catch((error) => console.log(`Error fetching flights: ${error}`))
|
||||
.then((response) => {
|
||||
if (response != null) {
|
||||
for (const flight of response.data) {
|
||||
dispatch(registerFlight(flight as Flight));
|
||||
}
|
||||
}
|
||||
});
|
||||
reloadGameState(dispatch);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ from . import (
|
||||
eventstream,
|
||||
flights,
|
||||
frontlines,
|
||||
game,
|
||||
mapzones,
|
||||
navmesh,
|
||||
qt,
|
||||
@ -27,6 +28,7 @@ app.include_router(debuggeometries.router)
|
||||
app.include_router(eventstream.router)
|
||||
app.include_router(flights.router)
|
||||
app.include_router(frontlines.router)
|
||||
app.include_router(game.router)
|
||||
app.include_router(mapzones.router)
|
||||
app.include_router(navmesh.router)
|
||||
app.include_router(qt.router)
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.server.leaflet import LeafletPoint
|
||||
from game.theater import ControlPoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
class ControlPointJs(BaseModel):
|
||||
@ -29,3 +34,9 @@ class ControlPointJs(BaseModel):
|
||||
destination=destination,
|
||||
sidc=str(control_point.sidc()),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def all_in_game(game: Game) -> list[ControlPointJs]:
|
||||
return [
|
||||
ControlPointJs.for_control_point(cp) for cp in game.theater.controlpoints
|
||||
]
|
||||
|
||||
@ -13,16 +13,15 @@ router: APIRouter = APIRouter(prefix="/control-points")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_control_points(game: Game = Depends(GameContext.get)) -> list[ControlPointJs]:
|
||||
control_points = []
|
||||
for control_point in game.theater.controlpoints:
|
||||
control_points.append(ControlPointJs.for_control_point(control_point))
|
||||
return control_points
|
||||
def list_control_points(
|
||||
game: Game = Depends(GameContext.require),
|
||||
) -> list[ControlPointJs]:
|
||||
return ControlPointJs.all_in_game(game)
|
||||
|
||||
|
||||
@router.get("/{cp_id}")
|
||||
def get_control_point(
|
||||
cp_id: int, game: Game = Depends(GameContext.get)
|
||||
cp_id: int, game: Game = Depends(GameContext.require)
|
||||
) -> ControlPointJs:
|
||||
cp = game.theater.find_control_point_by_id(cp_id)
|
||||
if cp is None:
|
||||
@ -35,7 +34,7 @@ def get_control_point(
|
||||
|
||||
@router.get("/{cp_id}/destination-in-range")
|
||||
def destination_in_range(
|
||||
cp_id: int, lat: float, lng: float, game: Game = Depends(GameContext.get)
|
||||
cp_id: int, lat: float, lng: float, game: Game = Depends(GameContext.require)
|
||||
) -> bool:
|
||||
cp = game.theater.find_control_point_by_id(cp_id)
|
||||
if cp is None:
|
||||
@ -50,7 +49,7 @@ def destination_in_range(
|
||||
|
||||
@router.put("/{cp_id}/destination")
|
||||
def set_destination(
|
||||
cp_id: int, destination: LeafletPoint, game: Game = Depends(GameContext.get)
|
||||
cp_id: int, destination: LeafletPoint, game: Game = Depends(GameContext.require)
|
||||
) -> None:
|
||||
cp = game.theater.find_control_point_by_id(cp_id)
|
||||
if cp is None:
|
||||
@ -79,7 +78,7 @@ def set_destination(
|
||||
|
||||
|
||||
@router.put("/{cp_id}/cancel-travel")
|
||||
def cancel_travel(cp_id: int, game: Game = Depends(GameContext.get)) -> None:
|
||||
def cancel_travel(cp_id: int, game: Game = Depends(GameContext.require)) -> None:
|
||||
cp = game.theater.find_control_point_by_id(cp_id)
|
||||
if cp is None:
|
||||
raise HTTPException(
|
||||
|
||||
@ -10,15 +10,19 @@ router: APIRouter = APIRouter(prefix="/debug/waypoint-geometries")
|
||||
|
||||
|
||||
@router.get("/hold/{flight_id}")
|
||||
def hold_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> HoldZonesJs:
|
||||
def hold_zones(
|
||||
flight_id: UUID, game: Game = Depends(GameContext.require)
|
||||
) -> HoldZonesJs:
|
||||
return HoldZonesJs.for_flight(game.db.flights.get(flight_id), game)
|
||||
|
||||
|
||||
@router.get("/ip/{flight_id}")
|
||||
def ip_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> IpZonesJs:
|
||||
def ip_zones(flight_id: UUID, game: Game = Depends(GameContext.require)) -> IpZonesJs:
|
||||
return IpZonesJs.for_flight(game.db.flights.get(flight_id), game)
|
||||
|
||||
|
||||
@router.get("/join/{flight_id}")
|
||||
def join_zones(flight_id: UUID, game: Game = Depends(GameContext.get)) -> JoinZonesJs:
|
||||
def join_zones(
|
||||
flight_id: UUID, game: Game = Depends(GameContext.require)
|
||||
) -> JoinZonesJs:
|
||||
return JoinZonesJs.for_flight(game.db.flights.get(flight_id), game)
|
||||
|
||||
@ -17,7 +17,11 @@ class GameContext:
|
||||
cls._game_model = game_model
|
||||
|
||||
@classmethod
|
||||
def get(cls) -> Game:
|
||||
def get(cls) -> Game | None:
|
||||
return cls._game_model.game
|
||||
|
||||
@classmethod
|
||||
def require(cls) -> Game:
|
||||
if cls._game_model.game is None:
|
||||
raise RuntimeError("GameContext has no Game set")
|
||||
return cls._game_model.game
|
||||
|
||||
@ -33,21 +33,38 @@ class GameUpdateEventsJs(BaseModel):
|
||||
deleted_front_lines: set[UUID]
|
||||
updated_tgos: set[UUID]
|
||||
updated_control_points: set[int]
|
||||
reset_on_map_center: LeafletLatLon | None
|
||||
game_unloaded: bool
|
||||
|
||||
@classmethod
|
||||
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
|
||||
def from_events(
|
||||
cls, events: GameUpdateEvents, game: Game | None
|
||||
) -> GameUpdateEventsJs:
|
||||
|
||||
# We still need to be able to send update events when there is no game loaded
|
||||
# because we need to send the unload event.
|
||||
new_combats = []
|
||||
updated_combats = []
|
||||
if game is not None:
|
||||
new_combats = [
|
||||
FrozenCombatJs.for_combat(c, game.theater) for c in events.new_combats
|
||||
]
|
||||
updated_combats = [
|
||||
FrozenCombatJs.for_combat(c, game.theater)
|
||||
for c in events.updated_combats
|
||||
]
|
||||
|
||||
recenter_map = None
|
||||
if events.reset_on_map_center is not None:
|
||||
recenter_map = events.reset_on_map_center.as_list()
|
||||
|
||||
return GameUpdateEventsJs(
|
||||
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
|
||||
],
|
||||
updated_combats=[
|
||||
FrozenCombatJs.for_combat(c, game.theater)
|
||||
for c in events.updated_combats
|
||||
],
|
||||
new_combats=new_combats,
|
||||
updated_combats=updated_combats,
|
||||
ended_combats=[c.id for c in events.ended_combats],
|
||||
navmesh_updates=events.navmesh_updates,
|
||||
unculled_zones_updated=events.unculled_zones_updated,
|
||||
@ -66,4 +83,6 @@ class GameUpdateEventsJs(BaseModel):
|
||||
deleted_front_lines=events.deleted_front_lines,
|
||||
updated_tgos=events.updated_tgos,
|
||||
updated_control_points=events.updated_control_points,
|
||||
reset_on_map_center=recenter_map,
|
||||
game_unloaded=events.game_unloaded,
|
||||
)
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.ato import Flight
|
||||
from game.ato.flightstate import InFlight
|
||||
from game.server.leaflet import LeafletPoint
|
||||
from game.server.waypoints.models import FlightWaypointJs
|
||||
from game.server.waypoints.routes import waypoints_for_flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.ato import Flight
|
||||
|
||||
|
||||
class FlightJs(BaseModel):
|
||||
id: UUID
|
||||
@ -37,3 +41,12 @@ class FlightJs(BaseModel):
|
||||
sidc=str(flight.sidc()),
|
||||
waypoints=waypoints,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def all_in_game(game: Game, with_waypoints: bool) -> 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, with_waypoints))
|
||||
return flights
|
||||
|
||||
@ -14,19 +14,16 @@ router: APIRouter = APIRouter(prefix="/flights")
|
||||
|
||||
@router.get("/")
|
||||
def list_flights(
|
||||
with_waypoints: bool = False, game: Game = Depends(GameContext.get)
|
||||
with_waypoints: bool = False, game: Game = Depends(GameContext.require)
|
||||
) -> 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, with_waypoints))
|
||||
return flights
|
||||
return FlightJs.all_in_game(game, with_waypoints)
|
||||
|
||||
|
||||
@router.get("/{flight_id}")
|
||||
def get_flight(
|
||||
flight_id: UUID, with_waypoints: bool = False, game: Game = Depends(GameContext.get)
|
||||
flight_id: UUID,
|
||||
with_waypoints: bool = False,
|
||||
game: Game = Depends(GameContext.require),
|
||||
) -> FlightJs:
|
||||
flight = game.db.flights.get(flight_id)
|
||||
return FlightJs.for_flight(flight, with_waypoints)
|
||||
@ -34,7 +31,7 @@ def get_flight(
|
||||
|
||||
@router.get("/{flight_id}/commit-boundary")
|
||||
def commit_boundary(
|
||||
flight_id: UUID, game: Game = Depends(GameContext.get)
|
||||
flight_id: UUID, game: Game = Depends(GameContext.require)
|
||||
) -> LeafletPoly:
|
||||
flight = game.db.flights.get(flight_id)
|
||||
if not isinstance(flight.flight_plan, PatrollingFlightPlan):
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.server.leaflet import LeafletPoint
|
||||
from game.theater import FrontLine
|
||||
from game.utils import nautical_miles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.theater import FrontLine
|
||||
|
||||
|
||||
class FrontLineJs(BaseModel):
|
||||
id: UUID
|
||||
@ -22,3 +26,7 @@ class FrontLineJs(BaseModel):
|
||||
front_line.attack_heading.left.degrees, nautical_miles(2).meters
|
||||
)
|
||||
return FrontLineJs(id=front_line.id, extents=[a.latlng(), b.latlng()])
|
||||
|
||||
@staticmethod
|
||||
def all_in_game(game: Game) -> list[FrontLineJs]:
|
||||
return [FrontLineJs.for_front_line(f) for f in game.theater.conflicts()]
|
||||
|
||||
@ -10,12 +10,12 @@ router: APIRouter = APIRouter(prefix="/front-lines")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_front_lines(game: Game = Depends(GameContext.get)) -> list[FrontLineJs]:
|
||||
return [FrontLineJs.for_front_line(f) for f in game.theater.conflicts()]
|
||||
def list_front_lines(game: Game = Depends(GameContext.require)) -> list[FrontLineJs]:
|
||||
return FrontLineJs.all_in_game(game)
|
||||
|
||||
|
||||
@router.get("/{front_line_id}")
|
||||
def get_front_line(
|
||||
front_line_id: UUID, game: Game = Depends(GameContext.get)
|
||||
front_line_id: UUID, game: Game = Depends(GameContext.require)
|
||||
) -> FrontLineJs:
|
||||
return FrontLineJs.for_front_line(game.db.front_lines.get(front_line_id))
|
||||
|
||||
1
game/server/game/__init__.py
Normal file
1
game/server/game/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .routes import router
|
||||
35
game/server/game/models.py
Normal file
35
game/server/game/models.py
Normal file
@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
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.supplyroutes.models import SupplyRouteJs
|
||||
from game.server.tgos.models import TgoJs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class GameJs(BaseModel):
|
||||
control_points: list[ControlPointJs]
|
||||
tgos: list[TgoJs]
|
||||
supply_routes: list[SupplyRouteJs]
|
||||
front_lines: list[FrontLineJs]
|
||||
flights: list[FlightJs]
|
||||
map_center: LeafletPoint
|
||||
|
||||
@staticmethod
|
||||
def from_game(game: Game) -> GameJs:
|
||||
return GameJs(
|
||||
control_points=ControlPointJs.all_in_game(game),
|
||||
tgos=TgoJs.all_in_game(game),
|
||||
supply_routes=SupplyRouteJs.all_in_game(game),
|
||||
front_lines=FrontLineJs.all_in_game(game),
|
||||
flights=FlightJs.all_in_game(game, with_waypoints=True),
|
||||
map_center=game.theater.terrain.map_view_default.position.latlng(),
|
||||
)
|
||||
14
game/server/game/routes.py
Normal file
14
game/server/game/routes.py
Normal file
@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from game import Game
|
||||
from game.server import GameContext
|
||||
from .models import GameJs
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/game")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def game_state(game: Game | None = Depends(GameContext.get)) -> GameJs | None:
|
||||
if game is None:
|
||||
return None
|
||||
return GameJs.from_game(game)
|
||||
@ -9,7 +9,7 @@ router: APIRouter = APIRouter(prefix="/map-zones")
|
||||
|
||||
|
||||
@router.get("/terrain")
|
||||
def get_terrain(game: Game = Depends(GameContext.get)) -> MapZonesJs:
|
||||
def get_terrain(game: Game = Depends(GameContext.require)) -> MapZonesJs:
|
||||
zones = game.theater.landmap
|
||||
if zones is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
@ -22,7 +22,9 @@ def get_terrain(game: Game = Depends(GameContext.get)) -> MapZonesJs:
|
||||
|
||||
|
||||
@router.get("/unculled")
|
||||
def get_unculled_zones(game: Game = Depends(GameContext.get)) -> list[UnculledZoneJs]:
|
||||
def get_unculled_zones(
|
||||
game: Game = Depends(GameContext.require),
|
||||
) -> list[UnculledZoneJs]:
|
||||
return [
|
||||
UnculledZoneJs(
|
||||
position=zone.latlng(), radius=game.settings.perf_culling_distance * 1000
|
||||
@ -32,7 +34,9 @@ def get_unculled_zones(game: Game = Depends(GameContext.get)) -> list[UnculledZo
|
||||
|
||||
|
||||
@router.get("/threats")
|
||||
def get_threat_zones(game: Game = Depends(GameContext.get)) -> ThreatZoneContainerJs:
|
||||
def get_threat_zones(
|
||||
game: Game = Depends(GameContext.require),
|
||||
) -> ThreatZoneContainerJs:
|
||||
return ThreatZoneContainerJs(
|
||||
blue=ThreatZonesJs.from_zones(game.threat_zone_for(player=True), game.theater),
|
||||
red=ThreatZonesJs.from_zones(game.threat_zone_for(player=False), game.theater),
|
||||
|
||||
@ -9,7 +9,9 @@ router: APIRouter = APIRouter(prefix="/navmesh")
|
||||
|
||||
|
||||
@router.get("/", response_model=list[NavMeshPolyJs])
|
||||
def get(for_player: bool, game: Game = Depends(GameContext.get)) -> list[NavMeshPolyJs]:
|
||||
def get(
|
||||
for_player: bool, game: Game = Depends(GameContext.require)
|
||||
) -> list[NavMeshPolyJs]:
|
||||
mesh = game.coalition_for(for_player).nav_mesh
|
||||
return [
|
||||
NavMeshPolyJs(
|
||||
|
||||
@ -11,7 +11,7 @@ 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),
|
||||
game: Game = Depends(GameContext.require),
|
||||
qt: QtCallbacks = Depends(QtContext.get),
|
||||
) -> None:
|
||||
qt.create_new_package(game.db.front_lines.get(front_line_id))
|
||||
@ -20,7 +20,7 @@ def new_front_line_package(
|
||||
@router.post("/create-package/tgo/{tgo_id}")
|
||||
def new_tgo_package(
|
||||
tgo_id: UUID,
|
||||
game: Game = Depends(GameContext.get),
|
||||
game: Game = Depends(GameContext.require),
|
||||
qt: QtCallbacks = Depends(QtContext.get),
|
||||
) -> None:
|
||||
qt.create_new_package(game.db.tgos.get(tgo_id))
|
||||
@ -29,7 +29,7 @@ def new_tgo_package(
|
||||
@router.post("/info/tgo/{tgo_id}")
|
||||
def show_tgo_info(
|
||||
tgo_id: UUID,
|
||||
game: Game = Depends(GameContext.get),
|
||||
game: Game = Depends(GameContext.require),
|
||||
qt: QtCallbacks = Depends(QtContext.get),
|
||||
) -> None:
|
||||
qt.show_tgo_info(game.db.tgos.get(tgo_id))
|
||||
@ -38,7 +38,7 @@ def show_tgo_info(
|
||||
@router.post("/create-package/control-point/{cp_id}")
|
||||
def new_cp_package(
|
||||
cp_id: int,
|
||||
game: Game = Depends(GameContext.get),
|
||||
game: Game = Depends(GameContext.require),
|
||||
qt: QtCallbacks = Depends(QtContext.get),
|
||||
) -> None:
|
||||
cp = game.theater.find_control_point_by_id(cp_id)
|
||||
@ -53,7 +53,7 @@ def new_cp_package(
|
||||
@router.post("/info/control-point/{cp_id}")
|
||||
def show_control_point_info(
|
||||
cp_id: int,
|
||||
game: Game = Depends(GameContext.get),
|
||||
game: Game = Depends(GameContext.require),
|
||||
qt: QtCallbacks = Depends(QtContext.get),
|
||||
) -> None:
|
||||
cp = game.theater.find_control_point_by_id(cp_id)
|
||||
|
||||
@ -81,3 +81,29 @@ class SupplyRouteJs(BaseModel):
|
||||
sea
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def all_in_game(game: Game) -> list[SupplyRouteJs]:
|
||||
seen = set()
|
||||
routes = []
|
||||
for control_point in game.theater.controlpoints:
|
||||
seen.add(control_point)
|
||||
for destination, route in control_point.convoy_routes.items():
|
||||
if destination in seen:
|
||||
continue
|
||||
routes.append(
|
||||
SupplyRouteJs.for_link(
|
||||
game, control_point, destination, list(route), sea=False
|
||||
)
|
||||
)
|
||||
for destination, route in control_point.shipping_lanes.items():
|
||||
if destination in seen:
|
||||
continue
|
||||
if not destination.is_friendly_to(control_point):
|
||||
continue
|
||||
routes.append(
|
||||
SupplyRouteJs.for_link(
|
||||
game, control_point, destination, list(route), sea=True
|
||||
)
|
||||
)
|
||||
return routes
|
||||
|
||||
@ -8,27 +8,7 @@ router: APIRouter = APIRouter(prefix="/supply-routes")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_supply_routes(game: Game = Depends(GameContext.get)) -> list[SupplyRouteJs]:
|
||||
seen = set()
|
||||
routes = []
|
||||
for control_point in game.theater.controlpoints:
|
||||
seen.add(control_point)
|
||||
for destination, route in control_point.convoy_routes.items():
|
||||
if destination in seen:
|
||||
continue
|
||||
routes.append(
|
||||
SupplyRouteJs.for_link(
|
||||
game, control_point, destination, list(route), sea=False
|
||||
)
|
||||
)
|
||||
for destination, route in control_point.shipping_lanes.items():
|
||||
if destination in seen:
|
||||
continue
|
||||
if not destination.is_friendly_to(control_point):
|
||||
continue
|
||||
routes.append(
|
||||
SupplyRouteJs.for_link(
|
||||
game, control_point, destination, list(route), sea=True
|
||||
)
|
||||
)
|
||||
return routes
|
||||
def list_supply_routes(
|
||||
game: Game = Depends(GameContext.require),
|
||||
) -> list[SupplyRouteJs]:
|
||||
return SupplyRouteJs.all_in_game(game)
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.server.leaflet import LeafletPoint
|
||||
from game.theater import TheaterGroundObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.theater import TheaterGroundObject
|
||||
|
||||
|
||||
class TgoJs(BaseModel):
|
||||
@ -44,3 +48,12 @@ class TgoJs(BaseModel):
|
||||
dead=tgo.is_dead,
|
||||
sidc=str(tgo.sidc()),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def all_in_game(game: Game) -> list[TgoJs]:
|
||||
tgos = []
|
||||
for control_point in game.theater.controlpoints:
|
||||
for tgo in control_point.connected_objectives:
|
||||
if not tgo.is_control_point:
|
||||
tgos.append(TgoJs.for_tgo(tgo))
|
||||
return tgos
|
||||
|
||||
@ -10,15 +10,10 @@ router: APIRouter = APIRouter(prefix="/tgos")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_tgos(game: Game = Depends(GameContext.get)) -> list[TgoJs]:
|
||||
tgos = []
|
||||
for control_point in game.theater.controlpoints:
|
||||
for tgo in control_point.connected_objectives:
|
||||
if not tgo.is_control_point:
|
||||
tgos.append(TgoJs.for_tgo(tgo))
|
||||
return tgos
|
||||
def list_tgos(game: Game = Depends(GameContext.require)) -> list[TgoJs]:
|
||||
return TgoJs.all_in_game(game)
|
||||
|
||||
|
||||
@router.get("/{tgo_id}")
|
||||
def get_tgo(tgo_id: UUID, game: Game = Depends(GameContext.get)) -> TgoJs:
|
||||
def get_tgo(tgo_id: UUID, game: Game = Depends(GameContext.require)) -> TgoJs:
|
||||
return TgoJs.for_tgo(game.db.tgos.get(tgo_id))
|
||||
|
||||
@ -36,7 +36,7 @@ def waypoints_for_flight(flight: Flight) -> list[FlightWaypointJs]:
|
||||
|
||||
@router.get("/{flight_id}", response_model=list[FlightWaypointJs])
|
||||
def all_waypoints_for_flight(
|
||||
flight_id: UUID, game: Game = Depends(GameContext.get)
|
||||
flight_id: UUID, game: Game = Depends(GameContext.require)
|
||||
) -> list[FlightWaypointJs]:
|
||||
return waypoints_for_flight(game.db.flights.get(flight_id))
|
||||
|
||||
@ -46,7 +46,7 @@ def set_position(
|
||||
flight_id: UUID,
|
||||
waypoint_idx: int,
|
||||
position: LeafletPoint,
|
||||
game: Game = Depends(GameContext.get),
|
||||
game: Game = Depends(GameContext.require),
|
||||
) -> None:
|
||||
from game.server import EventStream
|
||||
|
||||
|
||||
@ -5,8 +5,10 @@ from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from dcs import Point
|
||||
from dcs.mapping import LatLng
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.ato import Flight, Package
|
||||
from game.sim.combat import FrozenCombat
|
||||
from game.theater import ControlPoint, FrontLine, TheaterGroundObject
|
||||
@ -32,6 +34,8 @@ class GameUpdateEvents:
|
||||
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)
|
||||
reset_on_map_center: LatLng | None = None
|
||||
game_unloaded: bool = False
|
||||
shutting_down: bool = False
|
||||
|
||||
@property
|
||||
@ -121,6 +125,17 @@ class GameUpdateEvents:
|
||||
self.updated_control_points.add(control_point.id)
|
||||
return self
|
||||
|
||||
def game_loaded(self, game: Game | None) -> GameUpdateEvents:
|
||||
if game is None:
|
||||
self.game_unloaded = True
|
||||
self.reset_on_map_center = None
|
||||
else:
|
||||
self.reset_on_map_center = (
|
||||
game.theater.terrain.map_view_default.position.latlng()
|
||||
)
|
||||
self.game_unloaded = False
|
||||
return self
|
||||
|
||||
def shut_down(self) -> GameUpdateEvents:
|
||||
self.shutting_down = True
|
||||
return self
|
||||
|
||||
@ -21,6 +21,7 @@ from game.factions import FACTIONS
|
||||
from game.profiling import logged_duration
|
||||
from game.server import EventStream, Server
|
||||
from game.settings import Settings
|
||||
from game.sim import GameUpdateEvents
|
||||
from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings
|
||||
from pydcs_extensions import load_mods
|
||||
from qt_ui import (
|
||||
@ -58,6 +59,11 @@ def inject_custom_payloads(user_path: Path) -> None:
|
||||
PayloadDirectories.set_preferred(user_path / "MissionEditor" / "UnitPayloads")
|
||||
|
||||
|
||||
def on_game_load(game: Game | None) -> None:
|
||||
EventStream.drain()
|
||||
EventStream.put_nowait(GameUpdateEvents().game_loaded(game))
|
||||
|
||||
|
||||
def run_ui(game: Optional[Game], new_map: bool, dev: bool) -> None:
|
||||
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
|
||||
app = QApplication(sys.argv)
|
||||
@ -143,7 +149,7 @@ def run_ui(game: Optional[Game], new_map: bool, dev: bool) -> None:
|
||||
|
||||
# Apply CSS (need works)
|
||||
GameUpdateSignal()
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(EventStream.drain)
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(on_game_load)
|
||||
|
||||
# Start window
|
||||
window = QLiberationWindow(game, new_map)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user