mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Add websocket handling for selected flights.
This commit is contained in:
parent
6d29bfdf65
commit
8e8bbe84f3
19
client/package-lock.json
generated
19
client/package-lock.json
generated
@ -32,6 +32,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/axios": "^0.14.0",
|
"@types/axios": "^0.14.0",
|
||||||
"@types/leaflet": "^1.7.9",
|
"@types/leaflet": "^1.7.9",
|
||||||
|
"@types/websocket": "^1.0.5",
|
||||||
"electron": "^17.1.0",
|
"electron": "^17.1.0",
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
"react-scripts": "5.0.0",
|
"react-scripts": "5.0.0",
|
||||||
@ -4219,6 +4220,15 @@
|
|||||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
|
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/websocket": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.1",
|
"version": "8.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.1.tgz",
|
||||||
@ -21771,6 +21781,15 @@
|
|||||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
|
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/websocket": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/ws": {
|
"@types/ws": {
|
||||||
"version": "8.5.1",
|
"version": "8.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.1.tgz",
|
||||||
|
|||||||
@ -50,6 +50,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/axios": "^0.14.0",
|
"@types/axios": "^0.14.0",
|
||||||
"@types/leaflet": "^1.7.9",
|
"@types/leaflet": "^1.7.9",
|
||||||
|
"@types/websocket": "^1.0.5",
|
||||||
"electron": "^17.1.0",
|
"electron": "^17.1.0",
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
"react-scripts": "5.0.0",
|
"react-scripts": "5.0.0",
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import "./App.css";
|
|||||||
|
|
||||||
import { LatLng } from "leaflet";
|
import { LatLng } from "leaflet";
|
||||||
import LiberationMap from "./components/liberationmap";
|
import LiberationMap from "./components/liberationmap";
|
||||||
import useInitialGameState from "./api/useInitialGameState";
|
import useEventStream from "./hooks/useEventSteam";
|
||||||
|
import useInitialGameState from "./hooks/useInitialGameState";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const mapCenter: LatLng = new LatLng(25.58, 54.9);
|
const mapCenter: LatLng = new LatLng(25.58, 54.9);
|
||||||
|
|
||||||
useInitialGameState();
|
useInitialGameState();
|
||||||
|
useEventStream();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
|
|||||||
@ -4,4 +4,6 @@ export const backend = axios.create({
|
|||||||
baseURL: "http://[::1]:5000/",
|
baseURL: "http://[::1]:5000/",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const WEBSOCKET_URL = "ws://[::1]:5000/eventstream";
|
||||||
|
|
||||||
export default backend;
|
export default backend;
|
||||||
|
|||||||
36
client/src/api/eventstream.tsx
Normal file
36
client/src/api/eventstream.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { deselectFlight, selectFlight } from "./flightsSlice";
|
||||||
|
|
||||||
|
import { AppDispatch } from "../app/store";
|
||||||
|
import { Flight } from "./flight";
|
||||||
|
import { LatLng } from "leaflet";
|
||||||
|
|
||||||
|
// Placeholder. We don't use this yet. This is just here so we can flesh out the
|
||||||
|
// update events model.
|
||||||
|
interface FrozenCombat {}
|
||||||
|
|
||||||
|
interface GameUpdateEvents {
|
||||||
|
updated_flight_positions: { [id: string]: LatLng };
|
||||||
|
new_combats: FrozenCombat[];
|
||||||
|
updated_combats: FrozenCombat[];
|
||||||
|
ended_combats: string[];
|
||||||
|
navmesh_updates: boolean[];
|
||||||
|
unculled_zones_updated: boolean;
|
||||||
|
threat_zones_updated: boolean;
|
||||||
|
new_flights: Flight[];
|
||||||
|
updated_flights: string[];
|
||||||
|
deleted_flights: string[];
|
||||||
|
selected_flight: string | null;
|
||||||
|
deselected_flight: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleStreamedEvents = (
|
||||||
|
dispatch: AppDispatch,
|
||||||
|
events: GameUpdateEvents
|
||||||
|
) => {
|
||||||
|
if (events.deselected_flight) {
|
||||||
|
dispatch(deselectFlight());
|
||||||
|
}
|
||||||
|
if (events.selected_flight != null) {
|
||||||
|
dispatch(selectFlight(events.selected_flight));
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -6,11 +6,13 @@ import { RootState } from "../app/store";
|
|||||||
interface FlightsState {
|
interface FlightsState {
|
||||||
blue: { [id: string]: Flight };
|
blue: { [id: string]: Flight };
|
||||||
red: { [id: string]: Flight };
|
red: { [id: string]: Flight };
|
||||||
|
selected: Flight | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: FlightsState = {
|
const initialState: FlightsState = {
|
||||||
blue: {},
|
blue: {},
|
||||||
red: {},
|
red: {},
|
||||||
|
selected: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const flightsSlice = createSlice({
|
export const flightsSlice = createSlice({
|
||||||
@ -42,11 +44,23 @@ export const flightsSlice = createSlice({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
deselectFlight: (state) => {
|
||||||
|
state.selected = null;
|
||||||
|
},
|
||||||
|
selectFlight: (state, action: PayloadAction<string>) => {
|
||||||
|
const id = action.payload;
|
||||||
|
state.selected = state.blue[id];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { clearFlights, registerFlight, unregisterFlight } =
|
export const {
|
||||||
flightsSlice.actions;
|
clearFlights,
|
||||||
|
registerFlight,
|
||||||
|
unregisterFlight,
|
||||||
|
deselectFlight,
|
||||||
|
selectFlight,
|
||||||
|
} = flightsSlice.actions;
|
||||||
|
|
||||||
export const selectFlights = (state: RootState) => state.flights;
|
export const selectFlights = (state: RootState) => state.flights;
|
||||||
|
|
||||||
|
|||||||
@ -3,14 +3,25 @@ import { Polyline } from "react-leaflet";
|
|||||||
|
|
||||||
const BLUE_PATH = "#0084ff";
|
const BLUE_PATH = "#0084ff";
|
||||||
const RED_PATH = "#c85050";
|
const RED_PATH = "#c85050";
|
||||||
|
const SELECTED_PATH = "#ffff00";
|
||||||
|
|
||||||
interface FlightPlanProps {
|
interface FlightPlanProps {
|
||||||
flight: Flight;
|
flight: Flight;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pathColor = (props: FlightPlanProps) => {
|
||||||
|
if (props.selected) {
|
||||||
|
return SELECTED_PATH;
|
||||||
|
} else if (props.flight.blue) {
|
||||||
|
return BLUE_PATH;
|
||||||
|
} else {
|
||||||
|
return RED_PATH;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function FlightPlanPath(props: FlightPlanProps) {
|
function FlightPlanPath(props: FlightPlanProps) {
|
||||||
const color = props.flight.blue ? BLUE_PATH : RED_PATH;
|
const color = pathColor(props);
|
||||||
const waypoints = props.flight.waypoints;
|
const waypoints = props.flight.waypoints;
|
||||||
if (waypoints == null) {
|
if (waypoints == null) {
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Flight } from "../../api/flight";
|
||||||
import FlightPlan from "../flightplan";
|
import FlightPlan from "../flightplan";
|
||||||
import { LayerGroup } from "react-leaflet";
|
import { LayerGroup } from "react-leaflet";
|
||||||
import { selectFlights } from "../../api/flightsSlice";
|
import { selectFlights } from "../../api/flightsSlice";
|
||||||
@ -10,11 +11,29 @@ interface FlightPlansLayerProps {
|
|||||||
export default function FlightPlansLayer(props: FlightPlansLayerProps) {
|
export default function FlightPlansLayer(props: FlightPlansLayerProps) {
|
||||||
const atos = useAppSelector(selectFlights);
|
const atos = useAppSelector(selectFlights);
|
||||||
const flights = props.blue ? atos.blue : atos.red;
|
const flights = props.blue ? atos.blue : atos.red;
|
||||||
|
const isNotSelected = (flight: Flight) => {
|
||||||
|
if (atos.selected == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return atos.selected.id !== flight.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedFlight = atos.selected ? (
|
||||||
|
<FlightPlan key={atos.selected.id} flight={atos.selected} selected={true} />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayerGroup>
|
<LayerGroup>
|
||||||
{Object.values(flights).map((flight) => {
|
{Object.values(flights)
|
||||||
return <FlightPlan key={flight.id} flight={flight} selected={false} />;
|
.filter(isNotSelected)
|
||||||
|
.map((flight) => {
|
||||||
|
return (
|
||||||
|
<FlightPlan key={flight.id} flight={flight} selected={false} />
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
|
{selectedFlight}
|
||||||
</LayerGroup>
|
</LayerGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
36
client/src/components/socketprovider/socketprovider.tsx
Normal file
36
client/src/components/socketprovider/socketprovider.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { ReactChild, createContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { WEBSOCKET_URL } from "../../api/backend";
|
||||||
|
|
||||||
|
const socket = new WebSocket(WEBSOCKET_URL);
|
||||||
|
|
||||||
|
export const SocketContext = createContext(socket);
|
||||||
|
|
||||||
|
interface SocketProviderProps {
|
||||||
|
children: ReactChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SocketProvider = (props: SocketProviderProps) => {
|
||||||
|
const [ws, setWs] = useState<WebSocket>(socket);
|
||||||
|
useEffect(() => {
|
||||||
|
const onClose = () => {
|
||||||
|
setWs(new WebSocket(WEBSOCKET_URL));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = (error: Event) => {
|
||||||
|
console.log(`Websocket error: ${error}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.addEventListener("close", onClose);
|
||||||
|
ws.addEventListener("error", onError);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ws.removeEventListener("close", onClose);
|
||||||
|
ws.removeEventListener("error", onError);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SocketContext.Provider value={ws}>{props.children}</SocketContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
26
client/src/hooks/useEventSteam.ts
Normal file
26
client/src/hooks/useEventSteam.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
|
import { handleStreamedEvents } from "../api/eventstream";
|
||||||
|
import { useAppDispatch } from "../app/hooks";
|
||||||
|
import { useSocket } from "./useSocket";
|
||||||
|
|
||||||
|
export const useEventStream = () => {
|
||||||
|
const ws = useSocket();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const onMessage = useCallback(
|
||||||
|
(message) => {
|
||||||
|
handleStreamedEvents(dispatch, JSON.parse(message.data));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ws.addEventListener("message", onMessage);
|
||||||
|
return () => {
|
||||||
|
ws.removeEventListener("message", onMessage);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useEventStream;
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { ControlPoint } from "./controlpoint";
|
import { ControlPoint } from "../api/controlpoint";
|
||||||
import { Flight } from "./flight";
|
import { Flight } from "../api/flight";
|
||||||
import backend from "./backend";
|
import backend from "../api/backend";
|
||||||
import { registerFlight } from "./flightsSlice";
|
import { registerFlight } from "../api/flightsSlice";
|
||||||
import { setControlPoints } from "./controlPointsSlice";
|
import { setControlPoints } from "../api/controlPointsSlice";
|
||||||
import { useAppDispatch } from "../app/hooks";
|
import { useAppDispatch } from "../app/hooks";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
8
client/src/hooks/useSocket.ts
Normal file
8
client/src/hooks/useSocket.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { SocketContext } from "../components/socketprovider/socketprovider";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
export const useSocket = () => {
|
||||||
|
const socket = useContext(SocketContext);
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
};
|
||||||
@ -6,12 +6,15 @@ import App from "./App";
|
|||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
import { SocketProvider } from "./components/socketprovider/socketprovider";
|
||||||
import { store } from "./app/store";
|
import { store } from "./app/store";
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
<SocketProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</SocketProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user