feat: Implemented server mode

This commit is contained in:
Davide Passoni 2024-12-16 17:24:02 +01:00
parent 032b74b57b
commit c2d5d4ea17
20 changed files with 389 additions and 222 deletions

View File

@ -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}`);

View File

@ -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;
}
}

View File

@ -275,6 +275,7 @@ export const defaultMapLayers = {
export enum OlympusState {
NOT_INITIALIZED = "Not initialized",
SERVER = "Server",
LOGIN = "Login",
IDLE = "Idle",
MAIN_MENU = "Main menu",

View File

@ -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("/", ` <break time="0.5s"/> `)},`;
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 && `<break time="1s"/>`;
})
);
} 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("/", ` <break time="0.5s"/> `))},`;
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 && `<break time="1s"/>`;
})
);
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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
)
);
}
});
});
}
}

View File

@ -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) {

View File

@ -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 = {

View File

@ -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 (
<div
className={`
absolute left-0 top-0 z-50 h-full w-full flex-col bg-olympus-900 p-5
`}
>
<div className="flex-col content-center">
<h2 className="mb-10 text-3xl font-bold text-white">DCS Olympus server</h2>
<div className="flex flex-col">
<div className="flex gap-5 text-white">
<div className="w-64">Connected to DCS:</div>
<div>{serverStatus.connected? <FaCheck className={`
text-xl text-green-500
`}/> : <FaXmark className={`text-xl text-red-500`}/>}</div>
</div>
<div className="flex gap-5 text-white">
<div className="w-64">Server load:</div>
<div style={{color: loadColor}}>{serverStatus.load}</div>
</div>
<div className="flex gap-5 text-white">
<div className="w-64">Server framerate:</div>
<div style={{color: frameRateColor}}>{serverStatus.frameRate} fps</div>
</div>
<div className="flex gap-5 text-white">
<div className="w-64">Elapsed time:</div>
<div>{ETtimeString}</div>
</div>
<div className="flex gap-5 text-white">
<div className="w-64">Mission local time:</div>
<div>{MTtimeString}</div>
</div>
</div>
</div>
<img src="./images/olympus-500x500.png" className={`
absolute right-4 top-4 ml-auto flex h-24
`}></img>
</div>
);
}

View File

@ -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 (
<div
@ -67,50 +68,64 @@ export function UI() {
font-sans
`}
>
<Header />
{appState !== OlympusState.SERVER && (
<>
<Header />
</>
)}
<div className="flex h-full w-full flex-row-reverse">
<LoginModal open={appState === OlympusState.LOGIN} />
<ProtectionPromptModal open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
{appState === OlympusState.SERVER && <ServerOverlay />}
{appState !== OlympusState.SERVER && (
<>
<LoginModal open={appState === OlympusState.LOGIN} />
<ProtectionPromptModal open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
</>
)}
<div id="map-container" className="z-0 h-full w-screen" />
<MainMenu open={appState === OlympusState.MAIN_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
<SpawnMenu open={appState === OlympusState.SPAWN} onClose={() => getApp().setState(OlympusState.IDLE)} />
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)} />
<UnitControlMenu
open={
appState === OlympusState.UNIT_CONTROL &&
![UnitControlSubState.FORMATION, UnitControlSubState.UNIT_EXPLOSION_MENU].includes(appSubState as UnitControlSubState)
}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<FormationMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.FORMATION}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<DrawingMenu open={appState === OlympusState.DRAW} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AudioMenu open={appState === OlympusState.AUDIO} onClose={() => getApp().setState(OlympusState.IDLE)} />
<GameMasterMenu open={appState === OlympusState.GAME_MASTER} onClose={() => getApp().setState(OlympusState.IDLE)} />
<UnitExplosionMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
{/*}<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />
{appState !== OlympusState.SERVER && (
<>
<MainMenu open={appState === OlympusState.MAIN_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
<SpawnMenu open={appState === OlympusState.SPAWN} onClose={() => getApp().setState(OlympusState.IDLE)} />
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)} />
<UnitControlMenu
open={
appState === OlympusState.UNIT_CONTROL &&
![UnitControlSubState.FORMATION, UnitControlSubState.UNIT_EXPLOSION_MENU].includes(appSubState as UnitControlSubState)
}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<FormationMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.FORMATION}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<DrawingMenu open={appState === OlympusState.DRAW} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AudioMenu open={appState === OlympusState.AUDIO} onClose={() => getApp().setState(OlympusState.IDLE)} />
<GameMasterMenu open={appState === OlympusState.GAME_MASTER} onClose={() => getApp().setState(OlympusState.IDLE)} />
<UnitExplosionMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
{/*}<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AWACSMenu open={appState === OlympusState.AWACS} onClose={() => getApp().setState(OlympusState.IDLE)} />{*/}
<MiniMapPanel />
<ControlsPanel />
<CoordinatesPanel />
<RadiosSummaryPanel />
<MiniMapPanel />
<ControlsPanel />
<CoordinatesPanel />
<RadiosSummaryPanel />
<SideBar />
<InfoBar />
<HotGroupBar />
<SideBar />
<InfoBar />
<HotGroupBar />
<MapContextMenu />
<SpawnContextMenu />
<MapContextMenu />
<SpawnContextMenu />
</>
)}
</div>
</div>
);

View File

@ -7,7 +7,7 @@
{
"type": "node",
"request": "launch",
"name": "Launch Server (DCS)",
"name": "Launch server (core only)",
"skipFiles": [
"<node_internals>/**"
],
@ -26,75 +26,25 @@
"--vite"
],
"restart": true
},
},
{
"type": "node",
"request": "launch",
"name": "Launch Server (No DCS)",
"name": "Launch server (Electron)",
"skipFiles": [
"<node_internals>/**"
],
"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": [
"<node_internals>/**"
],
"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": [
"<node_internals>/**"
],
"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": [

View File

@ -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()
}
})
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});

View File

@ -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"
},

View File

@ -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: `<speak>${req.body.text}</speak>`},
voice: {languageCode: 'en-US', ssmlGender: 'MALE'},
audioConfig: {audioEncoding: 'MP3'},
};

View File

@ -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 {

View File

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