mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Started work on persistent session data
This commit is contained in:
parent
42e62be0f5
commit
5e40d7abf1
@ -1,4 +1,4 @@
|
||||
import { AudioMessageType } from "../constants/constants";
|
||||
import { AudioMessageType, OlympusState } from "../constants/constants";
|
||||
import { MicrophoneSource } from "./microphonesource";
|
||||
import { RadioSink } from "./radiosink";
|
||||
import { getApp } from "../olympusapp";
|
||||
@ -23,13 +23,14 @@ import {
|
||||
} from "../events";
|
||||
import { OlympusConfig } from "../interfaces";
|
||||
import { TextToSpeechSource } from "./texttospeechsource";
|
||||
import { SpeechController } from "./speechcontroller";
|
||||
|
||||
export class AudioManager {
|
||||
#audioContext: AudioContext;
|
||||
#synth = window.speechSynthesis;
|
||||
#devices: MediaDeviceInfo[] = [];
|
||||
#input: MediaDeviceInfo;
|
||||
#output: MediaDeviceInfo;
|
||||
#speechController: SpeechController;
|
||||
|
||||
/* The playback pipeline enables audio playback on the speakers/headphones */
|
||||
#playbackPipeline: PlaybackPipeline;
|
||||
@ -50,6 +51,7 @@ export class AudioManager {
|
||||
#guid: string = makeID(22);
|
||||
#SRSClientUnitIDs: number[] = [];
|
||||
#syncInterval: number;
|
||||
#speechRecognition: boolean = true;
|
||||
|
||||
constructor() {
|
||||
ConfigLoadedEvent.on((config: OlympusConfig) => {
|
||||
@ -70,6 +72,8 @@ export class AudioManager {
|
||||
altKey: false,
|
||||
});
|
||||
});
|
||||
|
||||
this.#speechController = new SpeechController();
|
||||
}
|
||||
|
||||
start() {
|
||||
@ -77,7 +81,6 @@ export class AudioManager {
|
||||
this.#syncRadioSettings();
|
||||
}, 1000);
|
||||
|
||||
this.#running = true;
|
||||
this.#audioContext = new AudioContext({ sampleRate: 16000 });
|
||||
|
||||
//@ts-ignore
|
||||
@ -124,7 +127,12 @@ export class AudioManager {
|
||||
audioPacket.getFrequencies().forEach((frequencyInfo) => {
|
||||
if (sink.getFrequency() === frequencyInfo.frequency && sink.getModulation() === frequencyInfo.modulation && sink.getTuned()) {
|
||||
sink.setReceiving(true);
|
||||
this.#playbackPipeline.playBuffer(audioPacket.getAudioData().buffer);
|
||||
|
||||
/* 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));
|
||||
sink.recordArrayBuffer(audioPacket.getAudioData().buffer);
|
||||
this.#playbackPipeline.playBuffer(dst);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -144,16 +152,40 @@ export class AudioManager {
|
||||
this.#sources.push(microphoneSource);
|
||||
AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources());
|
||||
|
||||
/* Add two default radios */
|
||||
this.addRadio();
|
||||
this.addRadio();
|
||||
let sessionRadios = getApp().getSessionDataManager().getSessionData().radios;
|
||||
if (sessionRadios) {
|
||||
/* Load session radios */
|
||||
sessionRadios.forEach((options) => {
|
||||
let newRadio = this.addRadio();
|
||||
newRadio?.setFrequency(options.frequency);
|
||||
newRadio?.setModulation(options.modulation);
|
||||
});
|
||||
} else {
|
||||
/* Add two default radios */
|
||||
this.addRadio();
|
||||
this.addRadio();
|
||||
}
|
||||
|
||||
let sessionUnitSinks = getApp().getSessionDataManager().getSessionData().unitSinks;
|
||||
if (sessionUnitSinks) {
|
||||
/* Load session radios */
|
||||
sessionUnitSinks.forEach((options) => {
|
||||
let unit = getApp().getUnitsManager().getUnitByID(options.ID);
|
||||
if (unit) {
|
||||
let newSink = this.addUnitSink(unit);
|
||||
}
|
||||
});
|
||||
}
|
||||
let sessionFileSources = getApp().getSessionDataManager().getSessionData().fileSources;
|
||||
if (sessionFileSources && sessionFileSources.length > 0) getApp().setState(OlympusState.LOAD_FILES);
|
||||
|
||||
this.#running = true;
|
||||
AudioManagerStateChangedEvent.dispatch(this.#running);
|
||||
});
|
||||
|
||||
const textToSpeechSource = new TextToSpeechSource();
|
||||
this.#sources.push(textToSpeechSource);
|
||||
|
||||
AudioManagerStateChangedEvent.dispatch(this.#running);
|
||||
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
this.#devices = devices;
|
||||
AudioManagerDevicesChangedEvent.dispatch(devices);
|
||||
@ -190,13 +222,10 @@ export class AudioManager {
|
||||
|
||||
addFileSource(file) {
|
||||
console.log(`Adding file source from ${file.name}`);
|
||||
if (!this.#running) {
|
||||
console.log("Audio manager not started, aborting...");
|
||||
return;
|
||||
}
|
||||
const newSource = new FileSource(file);
|
||||
this.#sources.push(newSource);
|
||||
AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources());
|
||||
return newSource;
|
||||
}
|
||||
|
||||
getSources() {
|
||||
@ -205,10 +234,6 @@ export class AudioManager {
|
||||
|
||||
removeSource(source: AudioSource) {
|
||||
console.log(`Removing source ${source.getName()}`);
|
||||
if (!this.#running) {
|
||||
console.log("Audio manager not started, aborting...");
|
||||
return;
|
||||
}
|
||||
source.disconnect();
|
||||
this.#sources = this.#sources.filter((v) => v != source);
|
||||
AudioSourcesChangedEvent.dispatch(this.#sources);
|
||||
@ -216,26 +241,22 @@ export class AudioManager {
|
||||
|
||||
addUnitSink(unit: Unit) {
|
||||
console.log(`Adding unit sink for unit with ID ${unit.ID}`);
|
||||
if (!this.#running) {
|
||||
console.log("Audio manager not started, aborting...");
|
||||
return;
|
||||
}
|
||||
this.#sinks.push(new UnitSink(unit));
|
||||
const newSink = new UnitSink(unit);
|
||||
this.#sinks.push(newSink);
|
||||
AudioSinksChangedEvent.dispatch(this.#sinks);
|
||||
return newSink;
|
||||
}
|
||||
|
||||
addRadio() {
|
||||
console.log("Adding new radio");
|
||||
if (!this.#running || this.#sources[0] === undefined) {
|
||||
console.log("Audio manager not started, aborting...");
|
||||
return;
|
||||
}
|
||||
const newRadio = new RadioSink();
|
||||
newRadio.speechDataAvailable = (blob) => this.#speechController.analyzeData(blob);
|
||||
this.#sinks.push(newRadio);
|
||||
/* Set radio name by default to be incremental number */
|
||||
newRadio.setName(`Radio ${this.#sinks.length}`);
|
||||
this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio);
|
||||
AudioSinksChangedEvent.dispatch(this.#sinks);
|
||||
return newRadio;
|
||||
}
|
||||
|
||||
getSinks() {
|
||||
@ -244,10 +265,6 @@ export class AudioManager {
|
||||
|
||||
removeSink(sink) {
|
||||
console.log(`Removing sink ${sink.getName()}`);
|
||||
if (!this.#running) {
|
||||
console.log("Audio manager not started, aborting...");
|
||||
return;
|
||||
}
|
||||
sink.disconnect();
|
||||
this.#sinks = this.#sinks.filter((v) => v != sink);
|
||||
let idx = 1;
|
||||
@ -309,12 +326,20 @@ export class AudioManager {
|
||||
this.#sources.find((source) => source instanceof TextToSpeechSource)?.playText(text);
|
||||
}
|
||||
|
||||
setSpeechRecognition(speechRecognition: boolean) {
|
||||
this.#speechRecognition = this.#speechRecognition;
|
||||
}
|
||||
|
||||
getSpeechRecognition() {
|
||||
return this.#speechRecognition;
|
||||
}
|
||||
|
||||
#syncRadioSettings() {
|
||||
/* Send the radio settings of each radio to the SRS backend */
|
||||
let message = {
|
||||
type: "Settings update",
|
||||
guid: this.#guid,
|
||||
coalition: 2,
|
||||
coalition: 2, // TODO
|
||||
settings: this.#sinks
|
||||
.filter((sink) => sink instanceof RadioSink)
|
||||
.map((radio) => {
|
||||
|
||||
@ -4,6 +4,7 @@ import { AudioSourcesChangedEvent } from "../events";
|
||||
|
||||
export class FileSource extends AudioSource {
|
||||
#file: File;
|
||||
#filename: string;
|
||||
#source: AudioBufferSourceNode;
|
||||
#duration: number = 0;
|
||||
#currentPosition: number = 0;
|
||||
@ -17,6 +18,7 @@ export class FileSource extends AudioSource {
|
||||
constructor(file) {
|
||||
super();
|
||||
this.#file = file;
|
||||
this.#filename = this.#file?.name;
|
||||
|
||||
this.setName(this.#file?.name ?? "N/A");
|
||||
|
||||
@ -117,4 +119,8 @@ export class FileSource extends AudioSource {
|
||||
getLooping() {
|
||||
return this.#looping;
|
||||
}
|
||||
|
||||
getFilename() {
|
||||
return this.#filename;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ import { AudioSink } from "./audiosink";
|
||||
import { AudioPacket } from "./audiopacket";
|
||||
import { getApp } from "../olympusapp";
|
||||
import { AudioSinksChangedEvent } from "../events";
|
||||
import { timeStamp } from "console";
|
||||
import { makeID } from "../other/utils";
|
||||
import { Recorder } from "./recorder";
|
||||
|
||||
/* Radio sink, basically implements a simple SRS Client in Olympus. Does not support encryption at this moment */
|
||||
export class RadioSink extends AudioSink {
|
||||
@ -19,10 +19,15 @@ export class RadioSink extends AudioSink {
|
||||
#clearReceivingTimeout: number;
|
||||
#packetID = 0;
|
||||
#guid = makeID(22);
|
||||
#recorder: Recorder;
|
||||
speechDataAvailable: (blob: Blob) => void;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.#recorder = new Recorder();
|
||||
this.#recorder.onRecordingCompleted = (blob) => this.speechDataAvailable(blob);
|
||||
|
||||
this.#encoder = new AudioEncoder({
|
||||
output: (data) => this.handleEncodedData(data),
|
||||
error: (e) => {
|
||||
@ -104,11 +109,19 @@ export class RadioSink extends AudioSink {
|
||||
|
||||
setReceiving(receiving) {
|
||||
// Only do it if actually changed
|
||||
if (receiving !== this.#receiving) AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks());
|
||||
if (receiving !== this.#receiving) {
|
||||
AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks());
|
||||
|
||||
if (getApp().getAudioManager().getSpeechRecognition()) {
|
||||
if (receiving) this.#recorder.start();
|
||||
else this.#recorder.stop();
|
||||
}
|
||||
}
|
||||
if (receiving) {
|
||||
window.clearTimeout(this.#clearReceivingTimeout);
|
||||
this.#clearReceivingTimeout = window.setTimeout(() => this.setReceiving(false), 500);
|
||||
}
|
||||
|
||||
this.#receiving = receiving;
|
||||
}
|
||||
|
||||
@ -124,11 +137,13 @@ export class RadioSink extends AudioSink {
|
||||
let audioPacket = new AudioPacket();
|
||||
audioPacket.setAudioData(new Uint8Array(arrayBuffer));
|
||||
audioPacket.setPacketID(this.#packetID++);
|
||||
audioPacket.setFrequencies([{
|
||||
audioPacket.setFrequencies([
|
||||
{
|
||||
frequency: this.#frequency,
|
||||
modulation: this.#modulation,
|
||||
encryption: 0
|
||||
}])
|
||||
encryption: 0,
|
||||
},
|
||||
]);
|
||||
audioPacket.setClientGUID(getApp().getAudioManager().getGuid());
|
||||
audioPacket.setTransmissionGUID(this.#guid);
|
||||
getApp().getAudioManager().send(audioPacket.toByteArray());
|
||||
@ -139,4 +154,8 @@ export class RadioSink extends AudioSink {
|
||||
this.#encoder.encode(audioData);
|
||||
audioData.close();
|
||||
}
|
||||
|
||||
recordArrayBuffer(arrayBuffer: ArrayBuffer) {
|
||||
this.#recorder.recordBuffer(arrayBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
77
frontend/react/src/audio/recorder.ts
Normal file
77
frontend/react/src/audio/recorder.ts
Normal file
@ -0,0 +1,77 @@
|
||||
export class Recorder {
|
||||
#decoder = new AudioDecoder({
|
||||
output: (chunk) => this.#handleDecodedData(chunk),
|
||||
error: (e) => console.log(e),
|
||||
});
|
||||
#trackGenerator: any; // TODO can we have typings?
|
||||
#writer: any;
|
||||
#gainNode: GainNode;
|
||||
#mediaRecorder: MediaRecorder;
|
||||
#recording = false;
|
||||
#chunks: any[] = [];
|
||||
onRecordingCompleted: (blob: Blob) => void
|
||||
|
||||
constructor() {
|
||||
this.#decoder.configure({
|
||||
codec: "opus",
|
||||
numberOfChannels: 1,
|
||||
sampleRate: 16000,
|
||||
//@ts-ignore // TODO why is this giving an error?
|
||||
opus: {
|
||||
frameDuration: 40000,
|
||||
},
|
||||
bitrateMode: "constant",
|
||||
});
|
||||
|
||||
//@ts-ignore
|
||||
this.#trackGenerator = new MediaStreamTrackGenerator({ kind: "audio" });
|
||||
this.#writer = this.#trackGenerator.writable.getWriter();
|
||||
|
||||
const stream = new MediaStream([this.#trackGenerator]);
|
||||
this.#mediaRecorder = new MediaRecorder(stream, {
|
||||
audioBitsPerSecond: 256000,
|
||||
mimeType: `audio/webm;codecs="opus"`,
|
||||
});
|
||||
}
|
||||
|
||||
recordBuffer(arrayBuffer) {
|
||||
if (!this.#recording) return;
|
||||
|
||||
const init = {
|
||||
type: "key",
|
||||
data: arrayBuffer,
|
||||
timestamp: 0,
|
||||
duration: 2000000,
|
||||
transfer: [arrayBuffer],
|
||||
};
|
||||
//@ts-ignore //TODO Typings?
|
||||
let encodedAudioChunk = new EncodedAudioChunk(init);
|
||||
|
||||
this.#decoder.decode(encodedAudioChunk);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.#mediaRecorder.start();
|
||||
this.#recording = true;
|
||||
this.#chunks = [];
|
||||
|
||||
this.#mediaRecorder.onstop = (e) => {
|
||||
if (this.#chunks.length > 0) this.onRecordingCompleted(this.#chunks[0]);
|
||||
};
|
||||
|
||||
this.#mediaRecorder.ondataavailable = (e) => {
|
||||
this.#chunks.push(e.data);
|
||||
};
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.#mediaRecorder.stop();
|
||||
this.#recording = false;
|
||||
}
|
||||
|
||||
#handleDecodedData(audioData) {
|
||||
this.#writer.ready.then(() => {
|
||||
this.#writer.write(audioData);
|
||||
});
|
||||
}
|
||||
}
|
||||
45
frontend/react/src/audio/speechcontroller.ts
Normal file
45
frontend/react/src/audio/speechcontroller.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { getApp } from "../olympusapp";
|
||||
import { blobToBase64 } from "../other/utils";
|
||||
|
||||
export class SpeechController {
|
||||
constructor() {}
|
||||
|
||||
analyzeData(blob: Blob) {
|
||||
blobToBase64(blob)
|
||||
.then((base64) => {
|
||||
const requestOptions = {
|
||||
method: "PUT", // Specify the request method
|
||||
headers: { "Content-Type": "application/json" }, // Specify the content type
|
||||
body: JSON.stringify({data: base64}), // Send the data in blob format
|
||||
};
|
||||
|
||||
fetch(getApp().getExpressAddress() + `/api/speech/recognize`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Speech recognized correctly`);
|
||||
return response.text();
|
||||
} else {
|
||||
getApp().addInfoMessage("Error recognizing speech");
|
||||
throw new Error("Error saving profile");
|
||||
}
|
||||
})
|
||||
.then((text) => this.#executeCommand(text))
|
||||
.catch((error) => console.error(error)); // Handle errors
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
}
|
||||
|
||||
#executeCommand(text) {
|
||||
console.log(`Received speech command: ${text}`);
|
||||
|
||||
if (text.indexOf("olympus") === 0 ) {
|
||||
this.#olympusCommand(text);
|
||||
} else if (text.indexOf(getApp().getAWACSController()?.getCallsign().toLowerCase()) === 0) {
|
||||
getApp().getAWACSController()?.executeCommand(text);
|
||||
}
|
||||
}
|
||||
|
||||
#olympusCommand(text) {
|
||||
|
||||
}
|
||||
}
|
||||
@ -287,6 +287,7 @@ export enum OlympusState {
|
||||
AUDIO = "Audio",
|
||||
AIRBASE = "Airbase",
|
||||
GAME_MASTER = "Game master",
|
||||
LOAD_FILES = "Load files"
|
||||
}
|
||||
|
||||
export const NO_SUBSTATE = "No substate";
|
||||
|
||||
107
frontend/react/src/controllers/awacs.ts
Normal file
107
frontend/react/src/controllers/awacs.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { getApp } from "../olympusapp";
|
||||
import { Coalition } from "../types/types";
|
||||
import { Unit } from "../unit/unit";
|
||||
import { bearing, coalitionToEnum, computeBearingRangeString, mToFt, rad2deg } from "../other/utils";
|
||||
|
||||
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 {
|
||||
#coalition: Coalition = "blue";
|
||||
#callsign: string = "Magic";
|
||||
#referenceUnit: Unit;
|
||||
|
||||
constructor() {}
|
||||
|
||||
executeCommand(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), ""));
|
||||
}
|
||||
}
|
||||
|
||||
createPicture(forTextToSpeech: boolean = false, unitName?: string) {
|
||||
let readout: string[] = [];
|
||||
|
||||
const mapOptions = getApp().getMap().getOptions();
|
||||
const activeGroups = Object.values(
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.computeClusters((unit) => unit.getCoalition() !== mapOptions.AWACSCoalition, 6) ?? {}
|
||||
);
|
||||
const bullseyes = getApp().getMissionManager().getBullseyes();
|
||||
const referenceUnit: Unit | undefined = unitName ? undefined : this.#referenceUnit; //TODO
|
||||
|
||||
if (bullseyes) {
|
||||
if (referenceUnit !== undefined) {
|
||||
readout.push(`${this.#callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s" : ""}`);
|
||||
readout.push(
|
||||
...activeGroups.map((group, idx) => {
|
||||
let order = "th";
|
||||
if (idx == 0) order = "st";
|
||||
else if (idx == 1) order = "nd";
|
||||
else if (idx == 2) order = "rd";
|
||||
|
||||
let trackDegs =
|
||||
bearing(group[0].getPosition().lat, group[0].getPosition().lng, referenceUnit.getPosition().lat, referenceUnit.getPosition().lng) -
|
||||
rad2deg(group[0].getTrack());
|
||||
if (trackDegs < 0) trackDegs += 360;
|
||||
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]}`;
|
||||
|
||||
if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey";
|
||||
else groupLine += ", hostile";
|
||||
|
||||
return groupLine;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
readout.push(`${this.#callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s" : ""}`);
|
||||
readout.push(
|
||||
...activeGroups.map((group, idx) => {
|
||||
let order = "th";
|
||||
if (idx == 0) order = "st";
|
||||
else if (idx == 1) order = "nd";
|
||||
else if (idx == 2) order = "rd";
|
||||
|
||||
let trackDegs = rad2deg(group[0].getTrack());
|
||||
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]}`;
|
||||
|
||||
if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey";
|
||||
else groupLine += ", hostile";
|
||||
|
||||
return groupLine;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return readout;
|
||||
}
|
||||
|
||||
createBogeyDope(forTextToSpeech: boolean = false, unitName: string) {}
|
||||
|
||||
setCallsign(callsign: string) {
|
||||
this.#callsign = callsign;
|
||||
}
|
||||
|
||||
getCallsign() {
|
||||
return this.#callsign;
|
||||
}
|
||||
|
||||
setCoalition(coalition: Coalition) {
|
||||
this.#coalition = coalition;
|
||||
}
|
||||
|
||||
getCoalition() {
|
||||
return this.#coalition;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { AudioSink } from "./audio/audiosink";
|
||||
import { AudioSource } from "./audio/audiosource";
|
||||
import { OlympusState, OlympusSubState } from "./constants/constants";
|
||||
import { CommandModeOptions, OlympusConfig, ServerStatus, SpawnRequestTable } from "./interfaces";
|
||||
import { CommandModeOptions, OlympusConfig, ServerStatus, SessionData, SpawnRequestTable } from "./interfaces";
|
||||
import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle";
|
||||
import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon";
|
||||
import { Airbase } from "./mission/airbase";
|
||||
@ -150,6 +150,19 @@ export class ModalEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionDataLoadedEvent {
|
||||
static on(callback: (sessionData: SessionData) => void) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.sessionData);
|
||||
});
|
||||
}
|
||||
|
||||
static dispatch(sessionData: SessionData) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: { sessionData } }));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
|
||||
/************** Map events ***************/
|
||||
export class MouseMovedEvent {
|
||||
static on(callback: (latlng: LatLng, elevation: number) => void) {
|
||||
|
||||
@ -29,9 +29,15 @@ export interface OlympusConfig {
|
||||
profiles?: ProfileOptions;
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
radios?: { frequency: number; modulation: number }[];
|
||||
fileSources?: { filename: string; volume: number }[];
|
||||
unitSinks?: {ID: number}[];
|
||||
}
|
||||
|
||||
export interface ProfileOptions {
|
||||
mapOptions: MapOptions,
|
||||
shortcuts: {[key: string]: ShortcutOptions}
|
||||
mapOptions: MapOptions;
|
||||
shortcuts: { [key: string]: ShortcutOptions };
|
||||
}
|
||||
|
||||
export interface ContextMenuOption {
|
||||
@ -100,7 +106,7 @@ export interface SpawnRequestTable {
|
||||
coalition: string;
|
||||
unit: UnitSpawnTable;
|
||||
amount: number;
|
||||
quickAccessName?: string
|
||||
quickAccessName?: string;
|
||||
}
|
||||
|
||||
export interface EffectRequestTable {
|
||||
|
||||
@ -37,7 +37,6 @@ export class MissionManager {
|
||||
AppStateChangedEvent.on((state, subState) => {
|
||||
if (this.getSelectedAirbase() !== null) AirbaseSelectedEvent.dispatch(null);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/** Update location of bullseyes
|
||||
|
||||
@ -23,6 +23,8 @@ import { AudioManager } from "./audio/audiomanager";
|
||||
import { 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 { SessionDataManager } from "./sessiondata";
|
||||
|
||||
export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
|
||||
export var IP = window.location.toString();
|
||||
@ -46,8 +48,12 @@ export class OlympusApp {
|
||||
#unitsManager: UnitsManager | null = null;
|
||||
#weaponsManager: WeaponsManager | null = null;
|
||||
#audioManager: AudioManager | null = null;
|
||||
#sessionDataManager: SessionDataManager | null = null;
|
||||
//#pluginsManager: // TODO
|
||||
|
||||
/* Controllers */
|
||||
#AWACSController: AWACSController | null = null;
|
||||
|
||||
constructor() {
|
||||
SelectedUnitsChangedEvent.on((selectedUnits) => {
|
||||
if (selectedUnits.length > 0) this.setState(OlympusState.UNIT_CONTROL);
|
||||
@ -86,12 +92,20 @@ export class OlympusApp {
|
||||
return this.#audioManager as AudioManager;
|
||||
}
|
||||
|
||||
getSessionDataManager() {
|
||||
return this.#sessionDataManager as SessionDataManager;
|
||||
}
|
||||
|
||||
/* TODO
|
||||
getPluginsManager() {
|
||||
return null // this.#pluginsManager as PluginsManager;
|
||||
}
|
||||
*/
|
||||
|
||||
getAWACSController() {
|
||||
return this.#AWACSController;
|
||||
}
|
||||
|
||||
getExpressAddress() {
|
||||
return `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}express`;
|
||||
}
|
||||
@ -103,6 +117,7 @@ export class OlympusApp {
|
||||
start() {
|
||||
/* Initialize base functionalitites */
|
||||
this.#shortcutManager = new ShortcutManager(); /* Keep first */
|
||||
this.#sessionDataManager = new SessionDataManager();
|
||||
|
||||
this.#map = new Map("map-container");
|
||||
|
||||
@ -112,6 +127,9 @@ export class OlympusApp {
|
||||
this.#weaponsManager = new WeaponsManager();
|
||||
this.#audioManager = new AudioManager();
|
||||
|
||||
/* Controllers */
|
||||
this.#AWACSController = new AWACSController();
|
||||
|
||||
/* Set the address of the server */
|
||||
this.getServerManager().setAddress(this.getBackendAddress());
|
||||
this.getAudioManager().setAddress(this.getExpressAddress());
|
||||
@ -207,7 +225,7 @@ export class OlympusApp {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Profile ${this.#profileName} reset correctly`);
|
||||
location.reload()
|
||||
location.reload();
|
||||
} else {
|
||||
this.addInfoMessage("Error resetting profile");
|
||||
throw new Error("Error resetting profile");
|
||||
@ -228,7 +246,7 @@ export class OlympusApp {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`All profiles reset correctly`);
|
||||
location.reload()
|
||||
location.reload();
|
||||
} else {
|
||||
this.addInfoMessage("Error resetting profiles");
|
||||
throw new Error("Error resetting profiles");
|
||||
@ -243,6 +261,10 @@ export class OlympusApp {
|
||||
else return null;
|
||||
}
|
||||
|
||||
getProfileName() {
|
||||
return this.#profileName;
|
||||
}
|
||||
|
||||
loadProfile() {
|
||||
const profile = this.getProfile();
|
||||
if (profile) {
|
||||
@ -280,4 +302,6 @@ export class OlympusApp {
|
||||
InfoPopupEvent.dispatch(this.#infoMessages);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -397,3 +397,11 @@ 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)}`;
|
||||
}
|
||||
|
||||
export function blobToBase64(blob) {
|
||||
return new Promise((resolve: (value: string) => void, _) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
@ -668,7 +668,10 @@ export class ServerManager {
|
||||
checkSessionHash(newSessionHash: string) {
|
||||
if (this.#sessionHash != null) {
|
||||
if (newSessionHash !== this.#sessionHash) location.reload();
|
||||
} else this.#sessionHash = newSessionHash;
|
||||
} else {
|
||||
this.#sessionHash = newSessionHash;
|
||||
getApp().getSessionDataManager().loadSessionData(newSessionHash);
|
||||
}
|
||||
}
|
||||
|
||||
setConnected(newConnected: boolean) {
|
||||
|
||||
112
frontend/react/src/sessiondata.ts
Normal file
112
frontend/react/src/sessiondata.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { FileSource } from "./audio/filesource";
|
||||
import { RadioSink } from "./audio/radiosink";
|
||||
import { UnitSink } from "./audio/unitsink";
|
||||
import { AudioSinksChangedEvent, AudioSourcesChangedEvent, SessionDataLoadedEvent as SessionDataChangedEvent } from "./events";
|
||||
import { SessionData } from "./interfaces";
|
||||
import { getApp } from "./olympusapp";
|
||||
|
||||
export class SessionDataManager {
|
||||
#sessionData: SessionData = {};
|
||||
#sessionHash: string = "";
|
||||
#saveSessionDataTimeout: number | null = null;
|
||||
|
||||
constructor() {
|
||||
AudioSinksChangedEvent.on((audioSinks) => {
|
||||
if (getApp().getAudioManager().isRunning()) {
|
||||
this.#sessionData.radios = audioSinks
|
||||
.filter((sink) => sink instanceof RadioSink)
|
||||
.map((radioSink) => {
|
||||
return {
|
||||
frequency: radioSink.getFrequency(),
|
||||
modulation: radioSink.getModulation(),
|
||||
};
|
||||
});
|
||||
|
||||
this.#sessionData.unitSinks = audioSinks
|
||||
.filter((sink) => sink instanceof UnitSink)
|
||||
.map((unitSink) => {
|
||||
return {
|
||||
ID: unitSink.getUnit().ID
|
||||
};
|
||||
});
|
||||
|
||||
this.#saveSessionData();
|
||||
}
|
||||
});
|
||||
|
||||
AudioSourcesChangedEvent.on((audioSources) => {
|
||||
if (getApp().getAudioManager().isRunning()) {
|
||||
this.#sessionData.fileSources = audioSources
|
||||
.filter((sink) => sink instanceof FileSource)
|
||||
.map((fileSource) => {
|
||||
return { filename: fileSource.getFilename(), volume: fileSource.getVolume() };
|
||||
});
|
||||
this.#saveSessionData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadSessionData(sessionHash?: string) {
|
||||
if (sessionHash) this.#sessionHash = sessionHash;
|
||||
if (this.#sessionHash === undefined) {
|
||||
console.error("Trying to load session data but no session hash provided");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestOptions = {
|
||||
method: "PUT", // Specify the request method
|
||||
headers: { "Content-Type": "application/json" }, // Specify the content type
|
||||
body: JSON.stringify({ sessionHash }), // Send the data in JSON format
|
||||
};
|
||||
|
||||
fetch(getApp().getExpressAddress() + `/resources/sessiondata/load/${getApp().getProfileName()}`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Session data for profile ${getApp().getProfileName()} and session hash ${sessionHash} loaded correctly`);
|
||||
return response.json();
|
||||
} else {
|
||||
getApp().addInfoMessage("No session data found for this profile");
|
||||
throw new Error("No session data found for this profile");
|
||||
}
|
||||
}) // Parse the response as JSON
|
||||
.then((sessionData) => {
|
||||
this.#sessionData = sessionData;
|
||||
this.#applySessionData();
|
||||
SessionDataChangedEvent.dispatch(this.#sessionData);
|
||||
})
|
||||
.catch((error) => console.error(error)); // Handle errors
|
||||
}
|
||||
|
||||
getSessionData() {
|
||||
return this.#sessionData;
|
||||
}
|
||||
|
||||
#saveSessionData() {
|
||||
if (this.#saveSessionDataTimeout) window.clearTimeout(this.#saveSessionDataTimeout);
|
||||
this.#saveSessionDataTimeout = window.setTimeout(() => {
|
||||
const requestOptions = {
|
||||
method: "PUT", // Specify the request method
|
||||
headers: { "Content-Type": "application/json" }, // Specify the content type
|
||||
body: JSON.stringify({ sessionHash: this.#sessionHash, sessionData: this.#sessionData }), // Send the data in JSON format
|
||||
};
|
||||
|
||||
fetch(getApp().getExpressAddress() + `/resources/sessiondata/save/${getApp().getProfileName()}`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Session data for profile ${getApp().getProfileName()} and session hash ${this.#sessionHash} saved correctly`);
|
||||
console.log(this.#sessionData);
|
||||
SessionDataChangedEvent.dispatch(this.#sessionData);
|
||||
} else {
|
||||
getApp().addInfoMessage("Error loading session data");
|
||||
throw new Error("Error loading session data");
|
||||
}
|
||||
}) // Parse the response as JSON
|
||||
.catch((error) => console.error(error)); // Handle errors
|
||||
this.#saveSessionDataTimeout = null;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
#applySessionData() {
|
||||
let asd = 1;
|
||||
}
|
||||
}
|
||||
129
frontend/react/src/ui/modals/filesourceloadprompt.tsx
Normal file
129
frontend/react/src/ui/modals/filesourceloadprompt.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight, faCheck, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { SessionDataLoadedEvent } from "../../events";
|
||||
|
||||
export function FileSourceLoadPrompt(props: { open: boolean }) {
|
||||
const [files, setFiles] = useState([] as { filename: string; volume: number }[]);
|
||||
const [loaded, setLoaded] = useState([] as boolean[]);
|
||||
|
||||
useEffect(() => {
|
||||
SessionDataLoadedEvent.on((sessionData) => {
|
||||
if (getApp().getState() === OlympusState.LOAD_FILES) return; // TODO don't like this, is hacky Should avoid reading state directly
|
||||
if (sessionData.fileSources) {
|
||||
setFiles([...sessionData.fileSources]);
|
||||
setLoaded(
|
||||
sessionData.fileSources.map((file) => {
|
||||
return false;
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
className={`
|
||||
inline-flex h-fit max-h-[800px] w-[600px] overflow-y-auto scroll-smooth
|
||||
bg-white p-10
|
||||
dark:bg-olympus-800
|
||||
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
|
||||
max-md:border-none
|
||||
`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-12">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Please, select the files for the following audio sources
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
Browsers can't automatically load files from your computer, therefore you must click on the following buttons to select the original files for each
|
||||
audio file source.
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
If you don't want to reload your audio sources, press "Skip".
|
||||
</span>
|
||||
<div className="mt-4 w-full">
|
||||
{files.map((fileData, idx) => {
|
||||
return (
|
||||
<div
|
||||
className={`flex w-full content-center justify-between gap-4`}
|
||||
>
|
||||
<span className={`my-auto truncate text-white`}>{fileData.filename}</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={loaded[idx] === true || (idx > 0 && loaded[idx - 1] == false)}
|
||||
data-disabled={loaded[idx] === true || (idx > 0 && loaded[idx - 1] == false)}
|
||||
onClick={() => {
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.click();
|
||||
input.onchange = (e: Event) => {
|
||||
let target = e.target as HTMLInputElement;
|
||||
if (target && target.files) {
|
||||
var file = target.files[0];
|
||||
getApp().getAudioManager().addFileSource(file).setVolume(fileData.volume);
|
||||
loaded[idx] = true;
|
||||
setLoaded([...loaded]);
|
||||
if (idx === loaded.length - 1) getApp().setState(OlympusState.IDLE);
|
||||
}
|
||||
};
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex cursor-pointer content-center
|
||||
items-center gap-2 rounded-sm bg-blue-600 px-5 py-2.5
|
||||
text-sm font-medium text-white
|
||||
data-[disabled="true"]:bg-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-800
|
||||
hover:bg-blue-700
|
||||
`}
|
||||
>
|
||||
{loaded[idx] ? "Loaded" : "Load"}
|
||||
<FontAwesomeIcon className={`my-auto`} icon={loaded[idx] ? faCheck : faUpload} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {getApp().setState(OlympusState.IDLE)}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center gap-2
|
||||
rounded-sm border-[1px] bg-blue-700 px-5 py-2.5 text-sm
|
||||
font-medium text-white
|
||||
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
|
||||
dark:hover:bg-gray-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Skip
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -14,10 +14,6 @@ import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
import { Unit } from "../../unit/unit";
|
||||
import { Bullseye } from "../../mission/bullseye";
|
||||
import { bearing, coalitionToEnum, computeBearingRangeString, mToFt, rad2deg } from "../../other/utils";
|
||||
|
||||
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 function AWACSMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [callsign, setCallsign] = useState("Magic");
|
||||
@ -35,61 +31,9 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
|
||||
UnitUpdatedEvent.on((unit) => setRefreshTime(Date.now()));
|
||||
}, []);
|
||||
|
||||
const activeGroups = Object.values(getApp()?.getUnitsManager().computeClusters((unit) => unit.getCoalition() !== mapOptions.AWACSCoalition, 6) ?? {});
|
||||
|
||||
|
||||
|
||||
/*Object.values(hotgroups).filter((hotgroup) => {
|
||||
return hotgroup.every((unit) => unit.getCoalition() !== mapOptions.AWACSCoalition);
|
||||
});*/
|
||||
|
||||
let readout: string[] = [];
|
||||
|
||||
if (bullseyes) {
|
||||
if (referenceUnit) {
|
||||
readout.push(`${callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s": ""}`);
|
||||
readout.push(
|
||||
...activeGroups.map((group, idx) => {
|
||||
let order = "th";
|
||||
if (idx == 0) order = "st";
|
||||
else if (idx == 1) order = "nd";
|
||||
else if (idx == 2) order = "rd";
|
||||
|
||||
let trackDegs = bearing(group[0].getPosition().lat, group[0].getPosition().lng, referenceUnit.getPosition().lat, referenceUnit.getPosition().lng) - rad2deg(group[0].getTrack())
|
||||
if (trackDegs < 0) trackDegs += 360
|
||||
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]}`;
|
||||
|
||||
if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey"
|
||||
else groupLine += ", hostile"
|
||||
|
||||
return groupLine;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
readout.push(`${callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s": ""}`);
|
||||
readout.push(
|
||||
...activeGroups.map((group, idx) => {
|
||||
let order = "th";
|
||||
if (idx == 0) order = "st";
|
||||
else if (idx == 1) order = "nd";
|
||||
else if (idx == 2) order = "rd";
|
||||
|
||||
let trackDegs = rad2deg(group[0].getTrack())
|
||||
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]}`;
|
||||
|
||||
if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey"
|
||||
else groupLine += ", hostile"
|
||||
|
||||
return groupLine;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu title={"AWACS Tools"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
|
||||
<div
|
||||
@ -143,6 +87,7 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
{/*}
|
||||
{activeGroups.length == 0 ? (
|
||||
<>No hotgroups</>
|
||||
) : (
|
||||
@ -154,6 +99,7 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
|
||||
<button onClick={() => getApp().getAudioManager().playText(readout.reduce((acc, line) => acc += " " + line, ""))}>Play</button>
|
||||
</>
|
||||
)}
|
||||
{*/}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
|
||||
@ -110,7 +110,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
navyunit: true,
|
||||
},
|
||||
});
|
||||
const [selectionBlueprint, setSelectionBlueprint] = useState(null as null | UnitBlueprint);
|
||||
const [selectionID, setSelectionID] = useState(null as null | number);
|
||||
const [searchBarRefState, setSearchBarRefState] = useState(null as MutableRefObject<null> | null);
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
@ -126,7 +126,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchBarRefState) setSearchBarRefState(searchBarRef);
|
||||
if (!props.open && selectionBlueprint !== null) setSelectionBlueprint(null);
|
||||
if (!props.open && selectionID !== null) setSelectionID(null);
|
||||
if (!props.open && filterString !== "") setFilterString("");
|
||||
});
|
||||
|
||||
@ -194,11 +194,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
|
||||
const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? [];
|
||||
|
||||
const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = [{}, {}, {}, {}, {}]; // TODOgetUnitsByLabel(filterString);
|
||||
|
||||
const mergedFilteredUnits = Object.assign({}, filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits) as {
|
||||
[key: string]: UnitBlueprint;
|
||||
};
|
||||
const filteredUnits = Object.values(getApp()?.getUnitsManager()?.getUnits() ?? {}).filter(
|
||||
(unit) => unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0
|
||||
);
|
||||
|
||||
const everyUnitIsGround = selectedCategories.every((category) => {
|
||||
return category === "GroundUnit";
|
||||
@ -249,51 +247,58 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
their specific type by using the search input.
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-olympus-600 p-4">
|
||||
<div
|
||||
className={`
|
||||
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
|
||||
`}
|
||||
>
|
||||
Control mode
|
||||
</div>
|
||||
<div className="flex flex-col justify-start gap-2">
|
||||
{Object.entries({
|
||||
human: ["Human", olButtonsVisibilityHuman],
|
||||
olympus: ["Olympus controlled", olButtonsVisibilityOlympus],
|
||||
dcs: ["From DCS mission", olButtonsVisibilityDcs],
|
||||
}).map((entry, idx) => {
|
||||
return (
|
||||
<div className="flex justify-between" key={idx}>
|
||||
<span className="font-light text-white">{entry[1][0] as string}</span>
|
||||
<OlToggle
|
||||
key={entry[0]}
|
||||
onClick={() => {
|
||||
selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]];
|
||||
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
|
||||
}}
|
||||
toggled={selectionFilter["control"][entry[0]]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{selectionID === null && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
|
||||
`}
|
||||
>
|
||||
Control mode
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
|
||||
`}
|
||||
>
|
||||
Types and coalitions
|
||||
</div>
|
||||
<div className="flex flex-col justify-start gap-2">
|
||||
{Object.entries({
|
||||
human: ["Human", olButtonsVisibilityHuman],
|
||||
olympus: ["Olympus controlled", olButtonsVisibilityOlympus],
|
||||
dcs: ["From DCS mission", olButtonsVisibilityDcs],
|
||||
}).map((entry, idx) => {
|
||||
return (
|
||||
<div className="flex justify-between" key={idx}>
|
||||
<span className="font-light text-white">{entry[1][0] as string}</span>
|
||||
<OlToggle
|
||||
key={entry[0]}
|
||||
onClick={() => {
|
||||
selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]];
|
||||
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
|
||||
}}
|
||||
toggled={selectionFilter["control"][entry[0]]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
|
||||
`}
|
||||
>
|
||||
Types and coalitions
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td className="pb-4 text-center font-bold text-blue-500">BLUE</td>
|
||||
<td className="pb-4 text-center font-bold text-gray-500">NEUTRAL</td>
|
||||
<td className="pb-4 text-center font-bold text-red-500">RED</td>
|
||||
</tr>
|
||||
{selectionBlueprint === null &&
|
||||
{selectionID === null && (
|
||||
<tr>
|
||||
<td></td>
|
||||
<td className="pb-4 text-center font-bold text-blue-500">BLUE</td>
|
||||
<td className="pb-4 text-center font-bold text-gray-500">NEUTRAL</td>
|
||||
<td className="pb-4 text-center font-bold text-red-500">RED</td>
|
||||
</tr>
|
||||
)}
|
||||
{selectionID === null &&
|
||||
Object.entries({
|
||||
aircraft: olButtonsVisibilityAircraft,
|
||||
helicopter: olButtonsVisibilityHelicopter,
|
||||
@ -311,7 +316,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<td className="text-center" key={coalition}>
|
||||
<OlCheckbox
|
||||
checked={selectionFilter[coalition][entry[0]]}
|
||||
disabled={selectionBlueprint !== null}
|
||||
disabled={selectionID !== null}
|
||||
onChange={() => {
|
||||
selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]];
|
||||
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
|
||||
@ -323,45 +328,47 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr>
|
||||
<td className="text-gray-200"></td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["blue"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["blue"]).some((value) => value);
|
||||
Object.keys(selectionFilter["blue"]).forEach((key) => {
|
||||
selectionFilter["blue"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["neutral"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value);
|
||||
Object.keys(selectionFilter["neutral"]).forEach((key) => {
|
||||
selectionFilter["neutral"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["red"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["red"]).some((value) => value);
|
||||
Object.keys(selectionFilter["red"]).forEach((key) => {
|
||||
selectionFilter["red"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{selectionID === null && (
|
||||
<tr>
|
||||
<td className="text-gray-200"></td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["blue"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["blue"]).some((value) => value);
|
||||
Object.keys(selectionFilter["blue"]).forEach((key) => {
|
||||
selectionFilter["blue"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["neutral"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value);
|
||||
Object.keys(selectionFilter["neutral"]).forEach((key) => {
|
||||
selectionFilter["neutral"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["red"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["red"]).some((value) => value);
|
||||
Object.keys(selectionFilter["red"]).forEach((key) => {
|
||||
selectionFilter["red"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
@ -369,29 +376,28 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<OlSearchBar
|
||||
onChange={(value) => {
|
||||
setFilterString(value);
|
||||
selectionBlueprint && setSelectionBlueprint(null);
|
||||
selectionID && setSelectionID(null);
|
||||
}}
|
||||
text={selectionBlueprint ? selectionBlueprint.label : filterString}
|
||||
text={selectionID ? (getApp().getUnitsManager().getUnitByID(selectionID)?.getUnitName() ?? "") : filterString}
|
||||
/>
|
||||
</div>
|
||||
<OlDropdown buttonRef={searchBarRefState} open={filterString !== "" && selectionBlueprint === null}>
|
||||
<OlDropdown buttonRef={searchBarRefState} open={filterString !== "" && selectionID === null}>
|
||||
<div className="max-h-48">
|
||||
{filterString !== "" &&
|
||||
Object.keys(mergedFilteredUnits).length > 0 &&
|
||||
Object.entries(mergedFilteredUnits).map((entry) => {
|
||||
const blueprint = entry[1];
|
||||
filteredUnits.length > 0 &&
|
||||
filteredUnits.map((unit) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
key={entry[0]}
|
||||
key={unit[0]}
|
||||
onClick={() => {
|
||||
setSelectionBlueprint(blueprint);
|
||||
setSelectionID(unit.ID);
|
||||
}}
|
||||
>
|
||||
{blueprint.label}
|
||||
{unit.getUnitName()}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
{Object.keys(mergedFilteredUnits).length == 0 && <span>No results</span>}
|
||||
{filteredUnits.length == 0 && <span>No results</span>}
|
||||
</div>
|
||||
</OlDropdown>
|
||||
</div>
|
||||
@ -409,19 +415,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
Object.values(getApp().getUnitsManager().getUnits()).forEach((unit) => {
|
||||
/* Check if the control type is respected, return if it is not */
|
||||
if (unit.getHuman() && !selectionFilter["control"]["human"]) return;
|
||||
|
||||
if (unit.isControlledByOlympus() && !selectionFilter["control"]["olympus"]) return;
|
||||
|
||||
if (!unit.isControlledByDCS() && !selectionFilter["control"]["dcs"]) return;
|
||||
|
||||
/* If a specific unit is being selected check that the label is correct, otherwise check if the unit type is active for the coalition */
|
||||
if (selectionBlueprint) {
|
||||
if (unit.getBlueprint()?.label === undefined || unit.getBlueprint()?.label !== selectionBlueprint.label) return;
|
||||
|
||||
/* This is a trick to easily reuse the same checkboxes used to globally enable unit types for a coalition,
|
||||
since those checkboxes are checked if at least one type is selected for a specific coalition.
|
||||
*/
|
||||
if (!Object.values(selectionFilter[unit.getCoalition()]).some((value) => value)) return;
|
||||
/* If a specific unit is being selected select the unit */
|
||||
if (selectionID) {
|
||||
if (unit.ID !== selectionID) return;
|
||||
} else {
|
||||
if (!selectionFilter[unit.getCoalition()][unit.getMarkerCategory()]) return;
|
||||
}
|
||||
@ -644,7 +643,6 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<OlButtonGroupItem
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setROE(ROEs[convertROE(idx)]);
|
||||
setSelectedUnitsData({
|
||||
@ -1180,9 +1178,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
value={activeAdvancedSettings ? activeAdvancedSettings.TACAN.channel : 1}
|
||||
></OlNumberInput>
|
||||
|
||||
<OlDropdown label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"} className={`
|
||||
my-auto w-20
|
||||
`}>
|
||||
<OlDropdown
|
||||
label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"}
|
||||
className={`my-auto w-20`}
|
||||
>
|
||||
<OlDropdownItem
|
||||
key={"X"}
|
||||
onClick={() => {
|
||||
@ -1301,9 +1300,11 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
className={`
|
||||
flex content-center gap-2 rounded-full
|
||||
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
|
||||
${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && `
|
||||
bg-yellow-700
|
||||
`}
|
||||
${
|
||||
selectedUnits[0].getFuel() > 10 &&
|
||||
selectedUnits[0].getFuel() <= 40 &&
|
||||
`bg-yellow-700`
|
||||
}
|
||||
${selectedUnits[0].getFuel() <= 10 && `bg-red-700`}
|
||||
px-2 py-1 text-sm font-bold text-white
|
||||
`}
|
||||
|
||||
@ -11,6 +11,7 @@ import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, SpawnSubState, UnitControlSubState } from "../constants/constants";
|
||||
import { getApp, setupApp } from "../olympusapp";
|
||||
import { LoginModal } from "./modals/loginmodal";
|
||||
import { FileSourceLoadPrompt } from "./modals/filesourceloadprompt";
|
||||
|
||||
import { MiniMapPanel } from "./panels/minimappanel";
|
||||
import { UnitControlBar } from "./panels/unitcontrolbar";
|
||||
@ -69,6 +70,7 @@ export function UI() {
|
||||
<LoginModal open={appState === OlympusState.LOGIN} />
|
||||
<ProtectionPromptModal open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
|
||||
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
|
||||
<FileSourceLoadPrompt open={appState === OlympusState.LOAD_FILES}/>
|
||||
|
||||
<div id="map-container" className="z-0 h-full w-screen" />
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import express = require("express");
|
||||
import fs = require("fs");
|
||||
var gtts = require("node-gtts")("en");
|
||||
|
||||
const gtts = require("node-gtts")("en");
|
||||
const speech = require("@google-cloud/speech");
|
||||
const router = express.Router();
|
||||
|
||||
module.exports = function () {
|
||||
@ -10,37 +9,30 @@ module.exports = function () {
|
||||
gtts.stream(req.body.text).pipe(res);
|
||||
});
|
||||
|
||||
router.get("/recognize", (req, res, next) => {
|
||||
//// Imports the Google Cloud client library
|
||||
//const speech = require("@google-cloud/speech");
|
||||
//
|
||||
//// Creates a client
|
||||
//const client = new speech.SpeechClient();
|
||||
//
|
||||
//// The path to the remote LINEAR16 file
|
||||
//const gcsUri = "gs://cloud-samples-data/speech/brooklyn_bridge.raw";
|
||||
//
|
||||
//// The audio file's encoding, sample rate in hertz, and BCP-47 language code
|
||||
//const audio = {
|
||||
// uri: gcsUri,
|
||||
//};
|
||||
//const config = {
|
||||
// encoding: "LINEAR16",
|
||||
// sampleRateHertz: 16000,
|
||||
// languageCode: "en-US",
|
||||
//};
|
||||
//const request = {
|
||||
// audio: audio,
|
||||
// config: config,
|
||||
//};
|
||||
//
|
||||
//// Detects speech in the audio file
|
||||
//client.recognize(request).then((response) => {
|
||||
// const transcription = response.results
|
||||
// .map((result) => result.alternatives[0].transcript)
|
||||
// .join("\n");
|
||||
// console.log(`Transcription: ${transcription}`);
|
||||
//});
|
||||
router.put("/recognize", (req, res, next) => {
|
||||
// Creates a client
|
||||
const client = new speech.SpeechClient();
|
||||
|
||||
// The audio file's encoding, sample rate in hertz, and BCP-47 language code
|
||||
const audio = {
|
||||
content: req.body.data.substring(req.body.data.indexOf("base64,") + 7),
|
||||
};
|
||||
const config = {
|
||||
encoding: "WEBM_OPUS",
|
||||
languageCode: "en-US"
|
||||
};
|
||||
const request = {
|
||||
audio: audio,
|
||||
config: config,
|
||||
};
|
||||
|
||||
// Detects speech in the audio file
|
||||
client.recognize(request).then((response) => {
|
||||
const transcription = response[0].results
|
||||
.map((result) => result.alternatives[0].transcript)
|
||||
.join("\n");
|
||||
res.send(transcription)
|
||||
}).catch((error) => res.sendStatus(400));
|
||||
});
|
||||
|
||||
return router;
|
||||
|
||||
@ -2,6 +2,9 @@ import express = require("express");
|
||||
import fs = require("fs");
|
||||
const router = express.Router();
|
||||
|
||||
let sessionHash = "";
|
||||
let sessionData = {}
|
||||
|
||||
module.exports = function (configLocation) {
|
||||
router.get("/config", function (req, res, next) {
|
||||
if (fs.existsSync(configLocation)) {
|
||||
@ -70,5 +73,29 @@ module.exports = function (configLocation) {
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/sessiondata/save/:profileName", function (req, res, next) {
|
||||
if (req.body.sessionHash === undefined || req.body.sessionData === undefined) res.sendStatus(400);
|
||||
let thisSessionHash = req.body.sessionHash;
|
||||
if (thisSessionHash !== sessionHash) {
|
||||
sessionHash = thisSessionHash;
|
||||
sessionData = {};
|
||||
}
|
||||
sessionData[req.params.profileName] = req.body.sessionData;
|
||||
res.end()
|
||||
})
|
||||
|
||||
router.put("/sessiondata/load/:profileName", function (req, res, next) {
|
||||
if (req.body.sessionHash === undefined) res.sendStatus(400);
|
||||
let thisSessionHash = req.body.sessionHash;
|
||||
if (thisSessionHash !== sessionHash) {
|
||||
sessionHash = thisSessionHash;
|
||||
sessionData = {};
|
||||
res.sendStatus(404);
|
||||
} else {
|
||||
res.send(sessionData[req.params.profileName]);
|
||||
res.end();
|
||||
}
|
||||
})
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user