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}
+ ]
}