From c2d5d4ea171152f0cb9b27f43c5a9cd7c0847ae3 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Mon, 16 Dec 2024 17:24:02 +0100 Subject: [PATCH] feat: Implemented server mode --- frontend/react/src/audio/audiomanager.ts | 17 ++- frontend/react/src/audio/radiosink.ts | 12 ++- frontend/react/src/constants/constants.ts | 1 + frontend/react/src/controllers/awacs.ts | 52 ++++++--- .../controller.ts} | 48 ++++----- .../src/controllers/controllermanager.ts | 15 +++ frontend/react/src/interfaces.ts | 12 ++- .../react/src/map/markers/custommarker.ts | 2 +- frontend/react/src/mission/airbase.ts | 2 +- frontend/react/src/olympusapp.ts | 68 +++++++++--- frontend/react/src/other/utils.ts | 18 +++- frontend/react/src/sessiondata.ts | 5 + frontend/react/src/ui/serveroverlay.tsx | 73 +++++++++++++ frontend/react/src/ui/ui.tsx | 91 +++++++++------- frontend/server/.vscode/launch.json | 66 ++---------- frontend/server/client.js | 100 +++++++++++------- frontend/server/package.json | 2 +- frontend/server/src/routes/api/speech.ts | 2 +- frontend/server/src/routes/resources.ts | 17 ++- olympus.json | 8 +- 20 files changed, 389 insertions(+), 222 deletions(-) rename frontend/react/src/{audio/speechcontroller.ts => controllers/controller.ts} (54%) create mode 100644 frontend/react/src/controllers/controllermanager.ts create mode 100644 frontend/react/src/ui/serveroverlay.tsx diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index ea7d7a57..5c967d07 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -23,14 +23,12 @@ import { } from "../events"; import { OlympusConfig } from "../interfaces"; import { TextToSpeechSource } from "./texttospeechsource"; -import { SpeechController } from "./speechcontroller"; export class AudioManager { #audioContext: AudioContext; #devices: MediaDeviceInfo[] = []; #input: MediaDeviceInfo; #output: MediaDeviceInfo; - #speechController: SpeechController; /* The playback pipeline enables audio playback on the speakers/headphones */ #playbackPipeline: PlaybackPipeline; @@ -72,8 +70,6 @@ export class AudioManager { altKey: false, }); }); - - this.#speechController = new SpeechController(); } start() { @@ -93,7 +89,7 @@ export class AudioManager { if (res === null) res = location.toString().match(/(?:http|https):\/\/(.+)/); let wsAddress = res ? res[1] : location.toString(); - if (wsAddress.at(wsAddress.length - 1) === "/") wsAddress = wsAddress.substring(0, wsAddress.length - 1) + if (wsAddress.at(wsAddress.length - 1) === "/") wsAddress = wsAddress.substring(0, wsAddress.length - 1); if (this.#endpoint) this.#socket = new WebSocket(`wss://${wsAddress}/${this.#endpoint}`); else if (this.#port) this.#socket = new WebSocket(`ws://${wsAddress}:${this.#port}`); else console.error("The audio backend was enabled but no port/endpoint was provided in the configuration"); @@ -127,6 +123,8 @@ export class AudioManager { if (sink.getFrequency() === frequencyInfo.frequency && sink.getModulation() === frequencyInfo.modulation && sink.getTuned()) { sink.setReceiving(true); + sink.setTransmittingUnit(getApp().getUnitsManager().getUnitByID(audioPacket.getUnitID()) ?? undefined) + /* Make a copy of the array buffer for the playback pipeline to use */ var dst = new ArrayBuffer(audioPacket.getAudioData().buffer.byteLength); new Uint8Array(dst).set(new Uint8Array(audioPacket.getAudioData().buffer)); @@ -164,7 +162,7 @@ export class AudioManager { let newRadio = this.addRadio(); this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio); this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio); - + newRadio = this.addRadio(); this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio); this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio); @@ -192,8 +190,8 @@ export class AudioManager { let sessionConnections = getApp().getSessionDataManager().getSessionData().connections; if (sessionConnections) { sessionConnections.forEach((connection) => { - this.#sources[connection[0]]?.connect(this.#sinks[connection[1]]); - }) + if (connection[0] < this.#sources.length && connection[1] < this.#sinks.length) this.#sources[connection[0]]?.connect(this.#sinks[connection[1]]); + }); } this.#running = true; @@ -208,7 +206,7 @@ export class AudioManager { AudioManagerDevicesChangedEvent.dispatch(devices); }); - this.#internalTextToSpeechSource = new TextToSpeechSource(); + this.#internalTextToSpeechSource = new TextToSpeechSource(); } stop() { @@ -265,7 +263,6 @@ export class AudioManager { addRadio() { console.log("Adding new radio"); const newRadio = new RadioSink(); - newRadio.speechDataAvailable = (blob) => this.#speechController.analyzeData(blob, newRadio); this.#sinks.push(newRadio); /* Set radio name by default to be incremental number */ newRadio.setName(`Radio ${this.#sinks.length}`); diff --git a/frontend/react/src/audio/radiosink.ts b/frontend/react/src/audio/radiosink.ts index 02bdc8eb..98777266 100644 --- a/frontend/react/src/audio/radiosink.ts +++ b/frontend/react/src/audio/radiosink.ts @@ -4,6 +4,7 @@ import { getApp } from "../olympusapp"; import { AudioSinksChangedEvent } from "../events"; import { makeID } from "../other/utils"; import { Recorder } from "./recorder"; +import { Unit } from "../unit/unit"; /* Radio sink, basically implements a simple SRS Client in Olympus. Does not support encryption at this moment */ export class RadioSink extends AudioSink { @@ -20,7 +21,8 @@ export class RadioSink extends AudioSink { #packetID = 0; #guid = makeID(22); #recorder: Recorder; - speechDataAvailable: (blob: Blob) => void; + #transmittingUnit: Unit | undefined; + speechDataAvailable: (blob: Blob) => void = (blob) => {}; constructor() { super(); @@ -158,4 +160,12 @@ export class RadioSink extends AudioSink { recordArrayBuffer(arrayBuffer: ArrayBuffer) { this.#recorder.recordBuffer(arrayBuffer); } + + setTransmittingUnit(transmittingUnit: Unit | undefined) { + this.#transmittingUnit = transmittingUnit; + } + + getTransmittingUnit() { + return this.#transmittingUnit; + } } diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 190f5abb..dd6aead3 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -275,6 +275,7 @@ export const defaultMapLayers = { export enum OlympusState { NOT_INITIALIZED = "Not initialized", + SERVER = "Server", LOGIN = "Login", IDLE = "Idle", MAIN_MENU = "Main menu", diff --git a/frontend/react/src/controllers/awacs.ts b/frontend/react/src/controllers/awacs.ts index af37d588..768ffa46 100644 --- a/frontend/react/src/controllers/awacs.ts +++ b/frontend/react/src/controllers/awacs.ts @@ -1,27 +1,30 @@ import { getApp } from "../olympusapp"; import { Coalition } from "../types/types"; import { Unit } from "../unit/unit"; -import { bearing, coalitionToEnum, computeBearingRangeString, mToFt, rad2deg } from "../other/utils"; +import { bearing, coalitionToEnum, computeBearingRangeString, mToFt, rad2deg, spellNumbers } from "../other/utils"; +import { Controller } from "./controller"; const trackStrings = ["North", "North-East", "East", "South-East", "South", "South-West", "West", "North-West", "North"]; const relTrackStrings = ["hot", "flank right", "beam right", "cold", "cold", "cold", "beam left", "flank left", "hot"]; -export class AWACSController { +export class AWACSController extends Controller { #coalition: Coalition = "blue"; #callsign: string = "Magic"; #referenceUnit: Unit; - constructor() { - + constructor(radioOptions: { frequency: number; modulation: number }, coalition: Coalition, callsign: string) { + super(radioOptions); + this.#coalition = coalition; + this.#callsign = callsign; } - executeCommand(text, radio) { + parseText(text) { if (text.indexOf("request picture") > 0) { console.log("Requested AWACS picture"); const readout = this.createPicture(true); - getApp() - .getAudioManager() - .playText(readout.reduce((acc, line) => (acc += " " + line), "")); + this.playText(readout.reduce((acc, line) => (acc += " " + line), "")); + } else if (text.indexOf("request bogey dope") > 0) { + } } @@ -54,16 +57,24 @@ export class AWACSController { if (trackDegs > 360) trackDegs -= 360; let trackIndex = Math.round(trackDegs / 45); - let groupLine = `${activeGroups.length > 1 ? idx + 1 + "" + order + " group" : "Single group"} bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${(mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, ${relTrackStrings[trackIndex]}`; + let groupLine = `${activeGroups.length > 1 ? idx + 1 + "" + order + " group" : "Single group"}.`; - if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey"; - else groupLine += ", hostile"; + if (forTextToSpeech) { + groupLine += `bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", ` `)},`; + groupLine += `${(mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, ${relTrackStrings[trackIndex]}`; + } else { + groupLine += `bullseye ${spellNumbers(computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " "))},`; + groupLine += `${(mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, ${relTrackStrings[trackIndex]}`; + } - return groupLine; + if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey."; + else groupLine += ", hostile."; + + return groupLine + forTextToSpeech && ``; }) ); } else { - readout.push(`${this.#callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s" : ""}`); + readout.push(`${this.#callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s" : ""}.`); readout.push( ...activeGroups.map((group, idx) => { let order = "th"; @@ -75,12 +86,19 @@ export class AWACSController { if (trackDegs < 0) trackDegs += 360; let trackIndex = Math.round(trackDegs / 45); - let groupLine = `${activeGroups.length > 1 ? idx + 1 + "" + order + " group" : "Single group"} bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${(mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, track ${trackStrings[trackIndex]}`; + let groupLine = `${activeGroups.length > 1 ? idx + 1 + "" + order + " group" : "Single group"}.`; + if (forTextToSpeech) { + groupLine += `bullseye ${spellNumbers(computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", ` `))},`; + groupLine += `${(mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, track ${trackStrings[trackIndex]}`; + } else { + groupLine += `bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")},`; + groupLine += `${(mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, track ${trackStrings[trackIndex]}`; + } - if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey"; - else groupLine += ", hostile"; + if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey."; + else groupLine += ", hostile."; - return groupLine; + return groupLine + forTextToSpeech && ``; }) ); } diff --git a/frontend/react/src/audio/speechcontroller.ts b/frontend/react/src/controllers/controller.ts similarity index 54% rename from frontend/react/src/audio/speechcontroller.ts rename to frontend/react/src/controllers/controller.ts index c97e6392..4a114f3a 100644 --- a/frontend/react/src/audio/speechcontroller.ts +++ b/frontend/react/src/controllers/controller.ts @@ -1,12 +1,20 @@ +import { RadioSink } from "../audio/radiosink"; import { getApp } from "../olympusapp"; import { blobToBase64 } from "../other/utils"; -import { RadioSink } from "./radiosink"; -export class SpeechController { +export abstract class Controller { + #radio: RadioSink; #playingText: boolean = false; - constructor() {} - analyzeData(blob: Blob, radio: RadioSink) { + constructor(radioOptions: { frequency: number; modulation: number }) { + this.#radio = getApp().getAudioManager().addRadio(); + this.#radio.setFrequency(radioOptions.frequency); + this.#radio.setModulation(radioOptions.modulation); + + this.#radio.speechDataAvailable = (blob) => this.analyzeData(blob) + } + + analyzeData(blob: Blob) { blobToBase64(blob) .then((base64) => { const requestOptions = { @@ -25,46 +33,28 @@ export class SpeechController { throw new Error("Error saving profile"); } }) - .then((text) => this.#executeCommand(text.toLowerCase(), radio)) + .then((text) => this.parseText(text.toLowerCase())) .catch((error) => console.error(error)); // Handle errors }) .catch((error) => console.error(error)); } - playText(text, radio: RadioSink) { + playText(text) { if (this.#playingText) return; this.#playingText = true; const textToSpeechSource = getApp().getAudioManager().getInternalTextToSpeechSource(); - textToSpeechSource.connect(radio); + textToSpeechSource.connect(this.#radio); textToSpeechSource.playText(text); - radio.setPtt(true); + this.#radio.setPtt(true); textToSpeechSource.onMessageCompleted = () => { this.#playingText = false; - radio.setPtt(false); - textToSpeechSource.disconnect(radio); + this.#radio.setPtt(false); + textToSpeechSource.disconnect(this.#radio); }; window.setTimeout(() => { this.#playingText = false; }, 30000); // Reset to false as failsafe } - #executeCommand(text, radio) { - console.log(`Received speech command: ${text}`); - - if (text.indexOf("olympus") === 0) { - this.#olympusCommand(text, radio); - } else if (text.indexOf(getApp().getAWACSController()?.getCallsign().toLowerCase()) === 0) { - getApp().getAWACSController()?.executeCommand(text, radio); - } - } - - #olympusCommand(text, radio) { - if (text.indexOf("request straight") > 0 || text.indexOf("request straightin") > 0) { - this.playText("Confirm you are on step 13, being a pussy?", radio); - } else if (text.indexOf("bolter") > 0) { - this.playText("What an idiot, I never boltered, 100% boarding rate", radio); - } else if (text.indexOf("read back") > 0) { - this.playText(text.replace("olympus", ""), radio); - } - } + abstract parseText(text: string): void; } diff --git a/frontend/react/src/controllers/controllermanager.ts b/frontend/react/src/controllers/controllermanager.ts new file mode 100644 index 00000000..e05f20c7 --- /dev/null +++ b/frontend/react/src/controllers/controllermanager.ts @@ -0,0 +1,15 @@ +import { Controller } from "./controller"; + +export class ControllerManager { + #controllers: Controller[] = []; + + constructor() {} + + getControllers() { + return this.#controllers; + } + + addController(controller: Controller) { + this.#controllers.push(controller); + } +} diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index c34e3e22..001a9d92 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -1,5 +1,5 @@ import { LatLng } from "leaflet"; -import { MapOptions } from "./types/types"; +import { Coalition, MapOptions } from "./types/types"; export interface OlympusConfig { frontend: { @@ -25,13 +25,23 @@ export interface OlympusConfig { mapMirrors: { [key: string]: string; }; + autoconnectWhenLocal: boolean; }; audio: { SRSPort: number; WSPort?: number; WSEndpoint?: string; }; + controllers: [ + {type: string, coalition: Coalition, frequency: number, modulation: number, callsign: string}, + ]; + local: boolean; profiles?: {[key: string]: ProfileOptions}; + authentication?: { + gameMasterPassword: string, + blueCommanderPasword: string, + redCommanderPassword: string + } } export interface SessionData { diff --git a/frontend/react/src/map/markers/custommarker.ts b/frontend/react/src/map/markers/custommarker.ts index 2e3a0f00..90403680 100644 --- a/frontend/react/src/map/markers/custommarker.ts +++ b/frontend/react/src/map/markers/custommarker.ts @@ -7,7 +7,7 @@ export class CustomMarker extends Marker { SelectionEnabledChangedEvent.on((enabled) => { const el = this.getElement(); - if (el === undefined) return; + if (el === undefined || el === null) return; if (enabled) el.classList.add("disable-pointer-events"); else el.classList.remove("disable-pointer-events"); }); diff --git a/frontend/react/src/mission/airbase.ts b/frontend/react/src/mission/airbase.ts index 0135fd05..5289500f 100644 --- a/frontend/react/src/mission/airbase.ts +++ b/frontend/react/src/mission/airbase.ts @@ -29,7 +29,7 @@ export class Airbase extends CustomMarker { AppStateChangedEvent.on((state, subState) => { const el = this.getElement(); - if (el === undefined) return; + if (el === undefined || el === null) return; if (state === OlympusState.IDLE || state === OlympusState.AIRBASE) el.classList.remove("airbase-disable-pointer-events"); else el.classList.add("airbase-disable-pointer-events"); }); diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index 5c134262..31635f14 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -20,11 +20,12 @@ import { WeaponsManager } from "./weapon/weaponsmanager"; import { ServerManager } from "./server/servermanager"; import { AudioManager } from "./audio/audiomanager"; -import { LoginSubState, NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants"; +import { GAME_MASTER, LoginSubState, NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants"; import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events"; -import { OlympusConfig, ProfileOptions } from "./interfaces"; -import { AWACSController } from "./controllers/awacs"; +import { OlympusConfig } from "./interfaces"; import { SessionDataManager } from "./sessiondata"; +import { ControllerManager } from "./controllers/controllermanager"; +import { AWACSController } from "./controllers/awacs"; export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}"; export var IP = window.location.toString(); @@ -48,11 +49,9 @@ export class OlympusApp { #weaponsManager: WeaponsManager; #audioManager: AudioManager; #sessionDataManager: SessionDataManager; + #controllerManager: ControllerManager; //#pluginsManager: // TODO - /* Controllers */ - #AWACSController: AWACSController; - constructor() { SelectedUnitsChangedEvent.on((selectedUnits) => { if (selectedUnits.length > 0) this.setState(OlympusState.UNIT_CONTROL); @@ -95,16 +94,16 @@ export class OlympusApp { return this.#sessionDataManager; } + getControllerManager() { + return this.#controllerManager; + } + /* TODO getPluginsManager() { return null // this.#pluginsManager as PluginsManager; } */ - getAWACSController() { - return this.#AWACSController; - } - start() { /* Initialize base functionalitites */ this.#shortcutManager = new ShortcutManager(); /* Keep first */ @@ -117,9 +116,7 @@ export class OlympusApp { this.#unitsManager = new UnitsManager(); this.#weaponsManager = new WeaponsManager(); this.#audioManager = new AudioManager(); - - /* Controllers */ - this.#AWACSController = new AWACSController(); + this.#controllerManager = new ControllerManager(); /* Check if we are running the latest version */ const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json"); @@ -143,8 +140,8 @@ export class OlympusApp { /* Load the config file from the server */ const configRequest = new Request("./resources/config", { headers: { - 'Cache-Control': 'no-cache', - } + "Cache-Control": "no-cache", + }, }); fetch(configRequest) @@ -166,7 +163,28 @@ export class OlympusApp { this.setState(OlympusState.LOGIN, LoginSubState.COMMAND_MODE); } } - if (this.getState() !== OlympusState.LOGIN) { + if (this.#config.local && this.#config.authentication) { + if (this.#config.frontend.autoconnectWhenLocal) { + this.getServerManager().setUsername("Game master"); + this.getServerManager().setPassword(this.#config.authentication.gameMasterPassword); + this.getServerManager().startUpdate(); + + const urlParams = new URLSearchParams(window.location.search); + const server = urlParams.get("server"); + if (server == null) { + this.setState(OlympusState.IDLE); + /* If no profile exists already with that name, create it from scratch from the defaults */ + if (this.getProfile() === null) this.saveProfile(); + /* Load the profile */ + this.loadProfile(); + } else { + this.setState(OlympusState.SERVER); + this.startServerMode(); + } + + this.getServerManager().setActiveCommandMode(GAME_MASTER); + } + } else if (this.getState() !== OlympusState.LOGIN) { this.setState(OlympusState.LOGIN, LoginSubState.CREDENTIALS); } ConfigLoadedEvent.dispatch(this.#config as OlympusConfig); @@ -301,4 +319,22 @@ export class OlympusApp { InfoPopupEvent.dispatch(this.#infoMessages); }, 5000); } + + startServerMode() { + ConfigLoadedEvent.on((config) => { + this.getAudioManager().start(); + + Object.values(config.controllers).forEach((controllerOptions) => { + if (controllerOptions.type.toLowerCase() === "awacs") { + this.getControllerManager().addController( + new AWACSController( + { frequency: controllerOptions.frequency, modulation: controllerOptions.modulation }, + controllerOptions.coalition, + controllerOptions.callsign + ) + ); + } + }); + }); + } } diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index e26f2c17..b7120bfe 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -72,7 +72,7 @@ export function keyEventWasInInput(event: KeyboardEvent) { } export const zeroAppend = function (num: number, places: number, decimal: boolean = false, decimalPlaces: number = 2) { - var string = decimal ? num.toFixed(decimalPlaces) : String(num); + var string = decimal ? num.toFixed(decimalPlaces) : num.toFixed(0); while (string.length < places) { string = "0" + string; } @@ -395,7 +395,21 @@ export function wait(time) { } export function computeBearingRangeString(latlng1, latlng2) { - return `${bearing(latlng1.lat, latlng1.lng, latlng2.lat, latlng2.lng).toFixed()}/${(latlng1.distanceTo(latlng2) / 1852).toFixed(0)}`; + return `${zeroAppend(bearing(latlng1.lat, latlng1.lng, latlng2.lat, latlng2.lng), 3)}/${zeroAppend(latlng1.distanceTo(latlng2) / 1852, 3)}`; +} + +export function spellNumbers(string: string) { + string = string.replaceAll("1", "one "); + string = string.replaceAll("2", "two "); + string = string.replaceAll("3", "three "); + string = string.replaceAll("4", "four "); + string = string.replaceAll("5", "five "); + string = string.replaceAll("6", "six "); + string = string.replaceAll("7", "seven "); + string = string.replaceAll("8", "eight "); + string = string.replaceAll("9", "nine "); + string = string.replaceAll("0", "zero "); + return string; } export function blobToBase64(blob) { diff --git a/frontend/react/src/sessiondata.ts b/frontend/react/src/sessiondata.ts index f2a58738..ffee8cba 100644 --- a/frontend/react/src/sessiondata.ts +++ b/frontend/react/src/sessiondata.ts @@ -2,6 +2,7 @@ import { AudioSink } from "./audio/audiosink"; import { FileSource } from "./audio/filesource"; import { RadioSink } from "./audio/radiosink"; import { UnitSink } from "./audio/unitsink"; +import { OlympusState } from "./constants/constants"; import { AudioSinksChangedEvent, AudioSourcesChangedEvent, SessionDataLoadedEvent as SessionDataChangedEvent } from "./events"; import { SessionData } from "./interfaces"; import { getApp } from "./olympusapp"; @@ -75,6 +76,8 @@ export class SessionDataManager { } loadSessionData(sessionHash?: string) { + if (getApp().getState() === OlympusState.SERVER) return; + if (sessionHash) this.#sessionHash = sessionHash; if (this.#sessionHash === undefined) { console.error("Trying to load session data but no session hash provided"); @@ -110,6 +113,8 @@ export class SessionDataManager { } #saveSessionData() { + if (getApp().getState() === OlympusState.SERVER) return; + if (this.#saveSessionDataTimeout) window.clearTimeout(this.#saveSessionDataTimeout); this.#saveSessionDataTimeout = window.setTimeout(() => { const requestOptions = { diff --git a/frontend/react/src/ui/serveroverlay.tsx b/frontend/react/src/ui/serveroverlay.tsx new file mode 100644 index 00000000..0385d2ad --- /dev/null +++ b/frontend/react/src/ui/serveroverlay.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from "react"; +import { ServerStatusUpdatedEvent } from "../events"; +import { ServerStatus } from "../interfaces"; +import { FaCheck, FaXmark } from "react-icons/fa6"; +import { zeroAppend } from "../other/utils"; + +export function ServerOverlay() { + const [serverStatus, setServerStatus] = useState({} as ServerStatus); + + useEffect(() => { + ServerStatusUpdatedEvent.on((status) => setServerStatus(status)); + }, []); + + let loadColor = "#8BFF63"; + if (serverStatus.load > 1000) loadColor = "#F05252"; + else if (serverStatus.load >= 100 && serverStatus.load < 1000) loadColor = "#FF9900"; + + let frameRateColor = "#8BFF63"; + if (serverStatus.frameRate < 30) frameRateColor = "#F05252"; + else if (serverStatus.frameRate >= 30 && serverStatus.frameRate < 60) frameRateColor = "#FF9900"; + + + const MThours = serverStatus.missionTime? serverStatus.missionTime.h: 0; + const MTminutes = serverStatus.missionTime? serverStatus.missionTime.m: 0; + const MTseconds = serverStatus.missionTime? serverStatus.missionTime.s: 0; + + const EThours = Math.floor((serverStatus.elapsedTime ?? 0) / 3600); + const ETminutes = Math.floor((serverStatus.elapsedTime ?? 0) / 60) % 60; + const ETseconds = Math.round(serverStatus.elapsedTime ?? 0) % 60; + + let MTtimeString = `${zeroAppend(MThours, 2)}:${zeroAppend(MTminutes, 2)}:${zeroAppend(MTseconds, 2)}`; + let ETtimeString = `${zeroAppend(EThours, 2)}:${zeroAppend(ETminutes, 2)}:${zeroAppend(ETseconds, 2)}`; + + return ( +
+
+

DCS Olympus server

+
+
+
Connected to DCS:
+
{serverStatus.connected? : }
+
+
+
Server load:
+
{serverStatus.load}
+
+
+
Server framerate:
+
{serverStatus.frameRate} fps
+
+
+
Elapsed time:
+
{ETtimeString}
+
+
+
Mission local time:
+
{MTtimeString}
+
+
+ +
+ +
+ ); +} diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index 2a0e67ea..21929d85 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -32,6 +32,7 @@ import { SpawnContextMenu } from "./contextmenus/spawncontextmenu"; import { CoordinatesPanel } from "./panels/coordinatespanel"; import { RadiosSummaryPanel } from "./panels/radiossummarypanel"; import { AWACSMenu } from "./panels/awacsmenu"; +import { ServerOverlay } from "./serveroverlay"; export type OlympusUIState = { mainMenuVisible: boolean; @@ -58,7 +59,7 @@ export function UI() { useEffect(() => { setupApp(); - }) + }); return (
-
+ {appState !== OlympusState.SERVER && ( + <> +
+ + )}
- - - - + {appState === OlympusState.SERVER && } + + {appState !== OlympusState.SERVER && ( + <> + + + + + )} +
- getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} - /> - getApp().setState(OlympusState.IDLE)} - /> - getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} - /> - {/*} getApp().setState(OlympusState.IDLE)} /> + {appState !== OlympusState.SERVER && ( + <> + getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} + /> + getApp().setState(OlympusState.IDLE)} + /> + getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} + /> + {/*} getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} />{*/} - - - - + + + + - - - + + + - - + + + + )}
); diff --git a/frontend/server/.vscode/launch.json b/frontend/server/.vscode/launch.json index 3c8a7300..b520013d 100644 --- a/frontend/server/.vscode/launch.json +++ b/frontend/server/.vscode/launch.json @@ -7,7 +7,7 @@ { "type": "node", "request": "launch", - "name": "Launch Server (DCS)", + "name": "Launch server (core only)", "skipFiles": [ "/**" ], @@ -26,75 +26,25 @@ "--vite" ], "restart": true - }, + }, { "type": "node", "request": "launch", - "name": "Launch Server (No DCS)", + "name": "Launch server (Electron)", "skipFiles": [ "/**" ], - "runtimeExecutable": "nodemon", + "runtimeExecutable": "npm", "runtimeArgs": [ - "--watch", - "src/**/*.ts", - "--exec", - "node", - "--inspect", - "-r", - "ts-node/register", - "src/www.ts", + "run", + "server", + "--", + "-s", "-c", "${input:enterDir}/Config/olympus.json", "--vite" ], - "restart": true, - "preLaunchTask": "demo-server" - }, - { - "type": "node", - "request": "launch", - "name": "Launch Server static Vite (DCS)", - "skipFiles": [ - "/**" - ], - "runtimeExecutable": "nodemon", - "runtimeArgs": [ - "--watch", - "src/**/*.ts", - "--exec", - "node", - "--inspect", - "-r", - "ts-node/register", - "src/www.ts", - "-c", - "${input:enterDir}/Config/olympus.json" - ], "restart": true - }, - { - "type": "node", - "request": "launch", - "name": "Launch Server static Vite (No DCS)", - "skipFiles": [ - "/**" - ], - "runtimeExecutable": "nodemon", - "runtimeArgs": [ - "--watch", - "src/**/*.ts", - "--exec", - "node", - "--inspect", - "-r", - "ts-node/register", - "src/www.ts", - "-c", - "${input:enterDir}/Config/olympus.json" - ], - "restart": true, - "preLaunchTask": "demo-server" } ], "inputs": [ diff --git a/frontend/server/client.js b/frontend/server/client.js index 1048b739..5283949e 100644 --- a/frontend/server/client.js +++ b/frontend/server/client.js @@ -1,57 +1,79 @@ -const { app, BrowserWindow } = require('electron/main') -const path = require('path') -const fs = require('fs') -const { spawn } = require('child_process'); -const yargs = require('yargs'); +const { app, BrowserWindow } = require("electron/main"); +const path = require("path"); +const fs = require("fs"); +const { spawn } = require("child_process"); +const yargs = require("yargs"); -yargs.alias('c', 'config').describe('c', 'olympus.json config location').string('rp'); +yargs + .alias("c", "config") + .describe("c", "olympus.json config location") + .string("rp"); + +yargs.alias("s", "server").describe("s", "run in server mode").string("rp"); args = yargs.argv; -console.log(`Config location: ${args["config"]}`) +console.log(`Config location: ${args["config"]}`); var frontendPort = 3000; +var serverMode = args["server"] ?? false; if (fs.existsSync(args["config"])) { - var json = JSON.parse(fs.readFileSync(args["config"], 'utf-8')); - frontendPort = json["frontend"]["port"]; + var json = JSON.parse(fs.readFileSync(args["config"], "utf-8")); + frontendPort = json["frontend"]["port"]; } else { - console.log("Failed to read config, trying default port"); + console.log("Failed to read config, trying default port"); } function createWindow() { - const win = new BrowserWindow({ - icon: "./../img/olympus.ico" - }) + const win = new BrowserWindow({ + icon: "./../img/olympus.ico", + }); - win.loadURL(`http://localhost:${frontendPort}`); - win.setMenuBarVisibility(false); - win.maximize(); + win.loadURL( + `http://localhost:${frontendPort}${serverMode ? "/?server" : ""}` + ); + win.setMenuBarVisibility(false); + + if (serverMode) { + } else { + win.maximize(); + } } -app.whenReady().then(() => { - const server = spawn('node', [path.join('.', 'build', 'www.js'), "--config", args["config"]]); +app + .whenReady() + .then(() => { + const server = spawn("node", [ + path.join(".", "build", "www.js"), + "--config", + args["config"], + "--vite", + ]); - server.stdout.on('data', (data) => { - console.log(`${data}`); - }); + server.stdout.on("data", (data) => { + console.log(`${data}`); + }); - server.stderr.on('data', (data) => { - console.error(`stderr: ${data}`); - }); + server.stderr.on("data", (data) => { + console.error(`stderr: ${data}`); + }); - server.on('close', (code) => { - console.log(`Child process exited with code ${code}`); - }); + server.on("close", (code) => { + console.log(`Child process exited with code ${code}`); + }); - createWindow() + createWindow(); - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow() - } - }) -}) + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); + }) + .catch((err) => { + console.error(err); + }); -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit() - } -}) \ No newline at end of file +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + } +}); diff --git a/frontend/server/package.json b/frontend/server/package.json index 26ac9163..9caf190b 100644 --- a/frontend/server/package.json +++ b/frontend/server/package.json @@ -4,7 +4,7 @@ "version": "{{OLYMPUS_VERSION_NUMBER}}", "scripts": { "build-release": "call ./scripts/build-release.bat", - "server": "node ./build/www.js", + "server": "electron .", "client": "electron .", "tsc": "tsc" }, diff --git a/frontend/server/src/routes/api/speech.ts b/frontend/server/src/routes/api/speech.ts index 2fd91e94..44db93a8 100644 --- a/frontend/server/src/routes/api/speech.ts +++ b/frontend/server/src/routes/api/speech.ts @@ -11,7 +11,7 @@ const generateClient = new textToSpeech.TextToSpeechClient(); module.exports = function () { router.put("/generate", (req, res, next) => { const request = { - input: {text: req.body.text}, + input: {ssml: `${req.body.text}`}, voice: {languageCode: 'en-US', ssmlGender: 'MALE'}, audioConfig: {audioEncoding: 'MP3'}, }; diff --git a/frontend/server/src/routes/resources.ts b/frontend/server/src/routes/resources.ts index 564b137c..a8c0f48b 100644 --- a/frontend/server/src/routes/resources.ts +++ b/frontend/server/src/routes/resources.ts @@ -23,13 +23,20 @@ module.exports = function (configLocation) { } if (fs.existsSync(configLocation)) { let rawdata = fs.readFileSync(configLocation, "utf-8"); + const local = ["127.0.0.1", "::ffff:127.0.0.1", "::1"].includes(req.connection.remoteAddress); const config = JSON.parse(rawdata); + let resConfig = { + frontend: { ...config.frontend }, + audio: { ...(config.audio ?? {}) }, + controllers: { ...(config.controllers ?? {}) }, + profiles: { ...(profiles ?? {}) }, + local: local, + }; + if (local) { + resConfig["authentication"] = config["authentication"] + } res.send( - JSON.stringify({ - frontend: { ...config.frontend }, - audio: { ...(config.audio ?? {}) }, - profiles: { ...(profiles ?? {}) }, - }) + JSON.stringify(resConfig) ); res.end(); } else { diff --git a/olympus.json b/olympus.json index 7f2b62de..d5960acf 100644 --- a/olympus.json +++ b/olympus.json @@ -37,11 +37,15 @@ "mapMirrors": { "DCS Map (Official)": "https://maps.dcsolympus.com/maps", "DCS Map (Alt.)": "https://refugees.dcsolympus.com/maps" - } + }, + "autoconnectWhenLocal": true }, "audio": { "SRSPort": 5002, "WSPort": 4000, "WSEndpoint": "audio" - } + }, + "controllers": [ + {"type": "awacs", "coalition": "blue", "callsign": "Magic", "frequency": 251000000, "modulation": 0} + ] }