mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
feat: Multiple improvements to audio backend
This commit is contained in:
parent
9cbfa2a8aa
commit
c9b143b5e0
@ -1,4 +1,4 @@
|
||||
import { AudioMessageType, BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../constants/constants";
|
||||
import { AudioManagerState, AudioMessageType, BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../constants/constants";
|
||||
import { MicrophoneSource } from "./microphonesource";
|
||||
import { RadioSink } from "./radiosink";
|
||||
import { getApp } from "../olympusapp";
|
||||
@ -16,6 +16,7 @@ import {
|
||||
AudioManagerInputChangedEvent,
|
||||
AudioManagerOutputChangedEvent,
|
||||
AudioManagerStateChangedEvent,
|
||||
AudioOptionsChangedEvent,
|
||||
AudioSinksChangedEvent,
|
||||
AudioSourcesChangedEvent,
|
||||
CommandModeOptionsChangedEvent,
|
||||
@ -24,13 +25,14 @@ import {
|
||||
} from "../events";
|
||||
import { CommandModeOptions, OlympusConfig } from "../interfaces";
|
||||
import { TextToSpeechSource } from "./texttospeechsource";
|
||||
import { Coalition } from "../types/types";
|
||||
import { AudioOptions, Coalition, SRSClientData } from "../types/types";
|
||||
|
||||
export class AudioManager {
|
||||
#audioContext: AudioContext;
|
||||
#devices: MediaDeviceInfo[] = [];
|
||||
#input: MediaDeviceInfo;
|
||||
#output: MediaDeviceInfo;
|
||||
#options: AudioOptions = { input: "", output: "" };
|
||||
|
||||
/* The audio sinks used to transmit the audio stream to the SRS backend */
|
||||
#sinks: AudioSink[] = [];
|
||||
@ -40,17 +42,19 @@ export class AudioManager {
|
||||
|
||||
/* The audio backend must be manually started so that the browser can detect the user is enabling audio.
|
||||
Otherwise, no playback will be performed. */
|
||||
#running: boolean = false;
|
||||
#state: string = AudioManagerState.STOPPED;
|
||||
#port: number;
|
||||
#endpoint: string;
|
||||
#socket: WebSocket | null = null;
|
||||
#guid: string = makeID(22);
|
||||
#SRSClientUnitIDs: number[] = [];
|
||||
#SRSClientsData: SRSClientData[] = [];
|
||||
#syncInterval: number;
|
||||
#speechRecognition: boolean = true;
|
||||
#internalTextToSpeechSource: TextToSpeechSource;
|
||||
#coalition: Coalition = "blue";
|
||||
#commandMode: string = BLUE_COMMANDER;
|
||||
#connectionCheckTimeout: number;
|
||||
#receivedPackets: number = 0;
|
||||
|
||||
constructor() {
|
||||
ConfigLoadedEvent.on((config: OlympusConfig) => {
|
||||
@ -86,15 +90,24 @@ export class AudioManager {
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.#state === AudioManagerState.ERROR) {
|
||||
console.error("The audio backend is in error state, cannot start");
|
||||
getApp().addInfoMessage("The audio backend is in error state, cannot start");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#state === AudioManagerState.RUNNING) {
|
||||
console.error("The audio backend is already running, cannot start again");
|
||||
}
|
||||
|
||||
getApp().addInfoMessage("Starting audio backend, please wait");
|
||||
|
||||
this.#syncInterval = window.setInterval(() => {
|
||||
this.#syncRadioSettings();
|
||||
}, 1000);
|
||||
|
||||
this.#audioContext = new AudioContext({ sampleRate: 16000 });
|
||||
|
||||
//@ts-ignore
|
||||
if (this.#output) this.#audioContext.setSinkId(this.#output.deviceId);
|
||||
|
||||
/* Connect the audio websocket */
|
||||
let res = location.toString().match(/(?:http|https):\/\/(.+):/);
|
||||
if (res === null) res = location.toString().match(/(?:http|https):\/\/(.+)/);
|
||||
@ -122,11 +135,77 @@ export class AudioManager {
|
||||
|
||||
/* Log any websocket errors */
|
||||
this.#socket.addEventListener("error", (event) => {
|
||||
console.log("An error occurred while connecting the WebSocket: " + event);
|
||||
console.log("An error occurred while connecting to the audio backend WebSocket");
|
||||
getApp().addInfoMessage("An error occurred while connecting to the audio backend WebSocket");
|
||||
this.error();
|
||||
});
|
||||
|
||||
/* Handle the reception of a new message */
|
||||
this.#socket.addEventListener("message", (event) => {
|
||||
this.#receivedPackets++;
|
||||
|
||||
/* Extract the clients data */
|
||||
event.data.arrayBuffer().then((packetArray) => {
|
||||
const packetUint8Array = new Uint8Array(packetArray);
|
||||
if (packetUint8Array[0] === MessageType.clientsData) {
|
||||
const newSRSClientsData = JSON.parse(new TextDecoder().decode(packetUint8Array.slice(1))).clientsData;
|
||||
|
||||
/* Check if anything has changed with the SRSClients */
|
||||
let clientsDataChanged = false;
|
||||
/* Check if the length of the clients data has changed */
|
||||
if (newSRSClientsData.length !== this.#SRSClientsData.length) {
|
||||
clientsDataChanged = true;
|
||||
} else {
|
||||
newSRSClientsData.forEach((newClientData) => {
|
||||
/* Check if the length is the same, but the clients names have changed */
|
||||
let clientData = this.#SRSClientsData.find((clientData) => newClientData.name === clientData.name);
|
||||
if (clientData === undefined) clientsDataChanged = true;
|
||||
else {
|
||||
/* Check if any of the data has changed */
|
||||
if (
|
||||
clientData.coalition !== newClientData.coalition ||
|
||||
clientData.unitID !== newClientData.unitID ||
|
||||
Object.keys(clientData.iff).find((key) => clientData.iff[key] !== newClientData.iff[key]) !== undefined ||
|
||||
clientData.radios.find(
|
||||
(radio, idx) => radio.frequency !== newClientData.radios[idx].frequency || radio.modulation !== newClientData.radios[idx].modulation
|
||||
) !== undefined
|
||||
)
|
||||
clientsDataChanged = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* If the clients data has changed, dispatch the event */
|
||||
if (clientsDataChanged) {
|
||||
this.#SRSClientsData = newSRSClientsData;
|
||||
SRSClientsChangedEvent.dispatch(this.#SRSClientsData);
|
||||
}
|
||||
|
||||
/* Update the number of connected clients for each radio */
|
||||
this.#sinks
|
||||
.filter((sink) => sink instanceof RadioSink)
|
||||
.forEach((radio) => {
|
||||
let connectedClients = 0;
|
||||
/* Check if any of the radios of this client is tuned to the same frequency, has the same modulation, and is of the same coalition */
|
||||
this.#SRSClientsData.forEach((clientData: SRSClientData) => {
|
||||
let clientConnected = false;
|
||||
clientData.radios.forEach((radioData) => {
|
||||
if (
|
||||
clientData.coalition === coalitionToEnum(this.#coalition) &&
|
||||
radioData.frequency === radio.getFrequency() &&
|
||||
radioData.modulation === radio.getModulation()
|
||||
)
|
||||
clientConnected = true;
|
||||
});
|
||||
if (clientConnected) connectedClients++;
|
||||
});
|
||||
|
||||
radio.setConnectedClients(connectedClients);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* Iterate over the radios. We iterate over the radios first so that a new copy of the audio packet is created for each pipeline */
|
||||
this.#sinks.forEach(async (sink) => {
|
||||
if (sink instanceof RadioSink) {
|
||||
/* Extract the audio data as array */
|
||||
@ -151,100 +230,135 @@ export class AudioManager {
|
||||
sink.playBuffer(dst);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.#SRSClientUnitIDs = JSON.parse(new TextDecoder().decode(packetUint8Array.slice(1))).unitIDs;
|
||||
SRSClientsChangedEvent.dispatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* Add the microphone source and connect it directly to the radio */
|
||||
const microphoneSource = new MicrophoneSource(this.#input);
|
||||
microphoneSource.initialize().then(() => {
|
||||
this.#sinks.forEach((sink) => {
|
||||
if (sink instanceof RadioSink) microphoneSource.connect(sink);
|
||||
});
|
||||
this.#sources.push(microphoneSource);
|
||||
AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources());
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
this.#devices = devices;
|
||||
AudioManagerDevicesChangedEvent.dispatch(devices);
|
||||
|
||||
let sessionRadios = getApp().getSessionDataManager().getSessionData().radios;
|
||||
if (sessionRadios) {
|
||||
/* Load session radios */
|
||||
sessionRadios.forEach((options) => {
|
||||
if (this.#options.input) {
|
||||
let newInput = this.#devices.find((device) => device.deviceId === this.#options.input);
|
||||
if (newInput) {
|
||||
this.#input = newInput;
|
||||
AudioManagerInputChangedEvent.dispatch(newInput);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#options.output) {
|
||||
let newOutput = this.#devices.find((device) => device.deviceId === this.#options.output);
|
||||
if (newOutput) {
|
||||
this.#output = newOutput;
|
||||
AudioManagerOutputChangedEvent.dispatch(newOutput);
|
||||
}
|
||||
}
|
||||
|
||||
/* Add the microphone source and connect it directly to the radio */
|
||||
const microphoneSource = new MicrophoneSource(this.#input);
|
||||
microphoneSource.initialize().then(() => {
|
||||
this.#sinks.forEach((sink) => {
|
||||
if (sink instanceof RadioSink) microphoneSource.connect(sink);
|
||||
});
|
||||
this.#sources.push(microphoneSource);
|
||||
AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources());
|
||||
|
||||
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);
|
||||
newRadio?.setPan(options.pan);
|
||||
});
|
||||
} else {
|
||||
/* Add two default radios and connect to the microphone*/
|
||||
let newRadio = this.addRadio();
|
||||
newRadio?.setFrequency(options.frequency);
|
||||
newRadio?.setModulation(options.modulation);
|
||||
newRadio?.setPan(options.pan);
|
||||
});
|
||||
} else {
|
||||
/* Add two default radios and connect to the microphone*/
|
||||
let newRadio = this.addRadio();
|
||||
this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio);
|
||||
this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio);
|
||||
newRadio.setPan(-1);
|
||||
newRadio.setPan(-1);
|
||||
|
||||
newRadio = this.addRadio();
|
||||
this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio);
|
||||
this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio);
|
||||
newRadio.setPan(1);
|
||||
}
|
||||
newRadio = this.addRadio();
|
||||
newRadio.setPan(1);
|
||||
}
|
||||
|
||||
let sessionFileSources = getApp().getSessionDataManager().getSessionData().fileSources;
|
||||
if (sessionFileSources) {
|
||||
/* Load file sources */
|
||||
sessionFileSources.forEach((options) => {
|
||||
this.addFileSource();
|
||||
});
|
||||
}
|
||||
let sessionFileSources = getApp().getSessionDataManager().getSessionData().fileSources;
|
||||
if (sessionFileSources) {
|
||||
/* Load file sources */
|
||||
sessionFileSources.forEach((options) => {
|
||||
this.addFileSource();
|
||||
});
|
||||
}
|
||||
|
||||
let sessionUnitSinks = getApp().getSessionDataManager().getSessionData().unitSinks;
|
||||
if (sessionUnitSinks) {
|
||||
/* Load unit sinks */
|
||||
sessionUnitSinks.forEach((options) => {
|
||||
let unit = getApp().getUnitsManager().getUnitByID(options.ID);
|
||||
if (unit) {
|
||||
this.addUnitSink(unit);
|
||||
}
|
||||
});
|
||||
}
|
||||
let sessionUnitSinks = getApp().getSessionDataManager().getSessionData().unitSinks;
|
||||
if (sessionUnitSinks) {
|
||||
/* Load unit sinks */
|
||||
sessionUnitSinks.forEach((options) => {
|
||||
let unit = getApp().getUnitsManager().getUnitByID(options.ID);
|
||||
if (unit) {
|
||||
this.addUnitSink(unit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let sessionConnections = getApp().getSessionDataManager().getSessionData().connections;
|
||||
if (sessionConnections) {
|
||||
sessionConnections.forEach((connection) => {
|
||||
if (connection[0] < this.#sources.length && connection[1] < this.#sinks.length) this.#sources[connection[0]]?.connect(this.#sinks[connection[1]]);
|
||||
});
|
||||
}
|
||||
let sessionConnections = getApp().getSessionDataManager().getSessionData().connections;
|
||||
if (sessionConnections) {
|
||||
sessionConnections.forEach((connection) => {
|
||||
if (connection[0] < this.#sources.length && connection[1] < this.#sinks.length) this.#sources[connection[0]]?.connect(this.#sinks[connection[1]]);
|
||||
});
|
||||
}
|
||||
|
||||
this.#running = true;
|
||||
AudioManagerStateChangedEvent.dispatch(this.#running);
|
||||
if (this.#state !== AudioManagerState.ERROR) {
|
||||
this.#state = AudioManagerState.RUNNING;
|
||||
AudioManagerStateChangedEvent.dispatch(this.#state);
|
||||
}
|
||||
});
|
||||
|
||||
//@ts-ignore
|
||||
if (this.#output) this.#audioContext.setSinkId(this.#output.deviceId);
|
||||
});
|
||||
|
||||
const textToSpeechSource = new TextToSpeechSource();
|
||||
this.#sources.push(textToSpeechSource);
|
||||
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
this.#devices = devices;
|
||||
AudioManagerDevicesChangedEvent.dispatch(devices);
|
||||
});
|
||||
|
||||
this.#internalTextToSpeechSource = new TextToSpeechSource();
|
||||
|
||||
/* Check if the audio backend is receiving updates from the backend every 10 seconds */
|
||||
this.#connectionCheckTimeout = window.setTimeout(() => {
|
||||
if (this.#receivedPackets === 0) {
|
||||
console.error("The audio backend is not receiving any data from the backend, stopping the audio backend");
|
||||
getApp().addInfoMessage("The audio backend is not receiving any data from the backend, stopping the audio backend");
|
||||
this.error();
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
stop() {
|
||||
/* Stop everything and send update event */
|
||||
this.#running = false;
|
||||
this.#sources.forEach((source) => source.disconnect());
|
||||
this.#sinks.forEach((sink) => sink.disconnect());
|
||||
this.#sources = [];
|
||||
this.#sinks = [];
|
||||
this.#socket?.close();
|
||||
|
||||
window.clearInterval(this.#connectionCheckTimeout);
|
||||
|
||||
window.clearInterval(this.#syncInterval);
|
||||
|
||||
AudioSourcesChangedEvent.dispatch(this.#sources);
|
||||
AudioSinksChangedEvent.dispatch(this.#sinks);
|
||||
AudioManagerStateChangedEvent.dispatch(this.#running);
|
||||
|
||||
if (this.#state !== AudioManagerState.ERROR) {
|
||||
this.#state = AudioManagerState.STOPPED;
|
||||
AudioManagerStateChangedEvent.dispatch(this.#state);
|
||||
}
|
||||
}
|
||||
|
||||
error() {
|
||||
this.stop();
|
||||
|
||||
this.#state = AudioManagerState.ERROR;
|
||||
AudioManagerStateChangedEvent.dispatch(this.#state);
|
||||
}
|
||||
|
||||
setPort(port) {
|
||||
@ -288,10 +402,24 @@ export class AudioManager {
|
||||
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);
|
||||
this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio);
|
||||
|
||||
AudioSinksChangedEvent.dispatch(this.#sinks);
|
||||
return newRadio;
|
||||
}
|
||||
|
||||
tuneNewRadio(frequency, modulation) {
|
||||
/* Check if a radio with the same frequency and modulation already exists */
|
||||
let radio = this.#sinks.find((sink) => sink instanceof RadioSink && sink.getFrequency() === frequency && sink.getModulation() === modulation);
|
||||
if (radio === undefined) {
|
||||
let newRadio = this.addRadio();
|
||||
newRadio.setFrequency(frequency);
|
||||
newRadio.setModulation(modulation);
|
||||
}
|
||||
}
|
||||
|
||||
getSinks() {
|
||||
return this.#sinks;
|
||||
}
|
||||
@ -325,12 +453,12 @@ export class AudioManager {
|
||||
return this.#audioContext;
|
||||
}
|
||||
|
||||
getSRSClientsUnitIDs() {
|
||||
return this.#SRSClientUnitIDs;
|
||||
getSRSClientsData() {
|
||||
return this.#SRSClientsData;
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
return this.#running;
|
||||
return this.#state;
|
||||
}
|
||||
|
||||
setInput(input: MediaDeviceInfo) {
|
||||
@ -339,6 +467,8 @@ export class AudioManager {
|
||||
AudioManagerInputChangedEvent.dispatch(input);
|
||||
this.stop();
|
||||
this.start();
|
||||
this.#options.input = input.deviceId;
|
||||
AudioOptionsChangedEvent.dispatch(this.#options);
|
||||
} else {
|
||||
console.error("Requested input device is not in devices list");
|
||||
}
|
||||
@ -350,6 +480,8 @@ export class AudioManager {
|
||||
AudioManagerOutputChangedEvent.dispatch(output);
|
||||
this.stop();
|
||||
this.start();
|
||||
this.#options.output = output.deviceId;
|
||||
AudioOptionsChangedEvent.dispatch(this.#options);
|
||||
} else {
|
||||
console.error("Requested output device is not in devices list");
|
||||
}
|
||||
@ -382,6 +514,14 @@ export class AudioManager {
|
||||
return this.#coalition;
|
||||
}
|
||||
|
||||
setOptions(options: AudioOptions) {
|
||||
this.#options = options;
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.#options;
|
||||
}
|
||||
|
||||
#syncRadioSettings() {
|
||||
/* Send the radio settings of each radio to the SRS backend */
|
||||
let message = {
|
||||
|
||||
@ -7,7 +7,7 @@ var packetID = 0;
|
||||
export enum MessageType {
|
||||
audio,
|
||||
settings,
|
||||
unitIDs
|
||||
clientsData
|
||||
}
|
||||
|
||||
export class AudioPacket {
|
||||
|
||||
@ -25,6 +25,7 @@ export class RadioSink extends AudioSink {
|
||||
#transmittingUnit: Unit | undefined;
|
||||
#pan: number = 0;
|
||||
#playbackPipeline: PlaybackPipeline;
|
||||
#connectedClients: number;
|
||||
speechDataAvailable: (blob: Blob) => void = (blob) => {};
|
||||
|
||||
constructor() {
|
||||
@ -190,4 +191,15 @@ export class RadioSink extends AudioSink {
|
||||
playBuffer(arrayBuffer) {
|
||||
this.#playbackPipeline.playBuffer(arrayBuffer);
|
||||
}
|
||||
|
||||
setConnectedClients(clientsNumber: number) {
|
||||
if (this.#connectedClients !== clientsNumber) {
|
||||
this.#connectedClients = clientsNumber;
|
||||
AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks());
|
||||
}
|
||||
}
|
||||
|
||||
getConnectedClients() {
|
||||
return this.#connectedClients;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { getApp } from "../olympusapp";
|
||||
import { Unit } from "../unit/unit";
|
||||
import { AudioUnitPipeline } from "./audiounitpipeline";
|
||||
import { AudioSinksChangedEvent, SRSClientsChangedEvent } from "../events";
|
||||
import { SRSClientData } from "../types/types";
|
||||
|
||||
/* Unit sink to implement a "loudspeaker" external sound. Useful for stuff like 5MC calls, air sirens,
|
||||
scramble calls and so on. Ideally, one may want to move this code to the backend*/
|
||||
@ -32,20 +33,23 @@ export class UnitSink extends AudioSink {
|
||||
#updatePipelines() {
|
||||
getApp()
|
||||
.getAudioManager()
|
||||
.getSRSClientsUnitIDs()
|
||||
.forEach((unitID) => {
|
||||
.getSRSClientsData()
|
||||
.forEach((clientData: SRSClientData) => {
|
||||
const unitID = clientData.unitID;
|
||||
if (unitID !== 0 && !(unitID in this.#unitPipelines)) {
|
||||
this.#unitPipelines[unitID] = new AudioUnitPipeline(this.#unit, unitID, this.getInputNode());
|
||||
this.#unitPipelines[unitID].setPtt(false);
|
||||
this.#unitPipelines[unitID].setMaxDistance(this.#maxDistance);
|
||||
console.log(`Added unit pipeline for unitID ${unitID} ` )
|
||||
console.log(`Added unit pipeline for unitID ${unitID} `);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(this.#unitPipelines).forEach((unitID) => {
|
||||
if (!(getApp().getAudioManager().getSRSClientsUnitIDs().includes(parseInt(unitID)))) {
|
||||
delete this.#unitPipelines[unitID];
|
||||
}
|
||||
const unitIDs = getApp()
|
||||
.getAudioManager()
|
||||
.getSRSClientsData()
|
||||
.map((clientData) => clientData.unitID);
|
||||
if (!unitIDs.includes(parseInt(unitID))) delete this.#unitPipelines[unitID];
|
||||
});
|
||||
}
|
||||
|
||||
@ -53,7 +57,7 @@ export class UnitSink extends AudioSink {
|
||||
this.#ptt = ptt;
|
||||
Object.values(this.#unitPipelines).forEach((pipeline) => {
|
||||
pipeline.setPtt(ptt);
|
||||
})
|
||||
});
|
||||
AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks());
|
||||
}
|
||||
|
||||
@ -65,7 +69,7 @@ export class UnitSink extends AudioSink {
|
||||
this.#maxDistance = maxDistance;
|
||||
Object.values(this.#unitPipelines).forEach((pipeline) => {
|
||||
pipeline.setMaxDistance(maxDistance);
|
||||
})
|
||||
});
|
||||
AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks());
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { LatLng, LatLngBounds } from "leaflet";
|
||||
import { Coalition, MapOptions } from "../types/types";
|
||||
import { MapOptions } from "../types/types";
|
||||
import { CommandModeOptions } from "../interfaces";
|
||||
import { ContextAction } from "../unit/contextaction";
|
||||
import {
|
||||
@ -981,3 +981,10 @@ export namespace ContextActions {
|
||||
{ type: ContextActionType.ADMIN, code: "KeyC", ctrlKey: false, shiftKey: false, altKey: false }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export enum AudioManagerState {
|
||||
STOPPED = "Stopped",
|
||||
RUNNING = "Running",
|
||||
ERROR = "Error"
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon";
|
||||
import { Airbase } from "./mission/airbase";
|
||||
import { Bullseye } from "./mission/bullseye";
|
||||
import { Shortcut } from "./shortcut/shortcut";
|
||||
import { Coalition, MapHiddenTypes, MapOptions } from "./types/types";
|
||||
import { AudioOptions, Coalition, MapHiddenTypes, MapOptions, SRSClientData } from "./types/types";
|
||||
import { ContextAction } from "./unit/contextaction";
|
||||
import { ContextActionSet } from "./unit/contextactionset";
|
||||
import { Unit } from "./unit/unit";
|
||||
@ -195,6 +195,22 @@ export class BindShortcutRequestEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export class AudioOptionsChangedEvent {
|
||||
static on(callback: (audioOptions: AudioOptions) => void, singleShot = false) {
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.audioOptions);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
static dispatch(audioOptions: AudioOptions) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: { audioOptions } }));
|
||||
if (DEBUG) console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
|
||||
export class ModalEvent {
|
||||
static on(callback: (modal: boolean) => void, singleShot = false) {
|
||||
document.addEventListener(
|
||||
@ -295,7 +311,7 @@ export class MapOptionsChangedEvent {
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(mapOptions: MapOptions, key?: (keyof MapOptions) | undefined) {
|
||||
static dispatch(mapOptions: MapOptions, key?: keyof MapOptions | undefined) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: { mapOptions, key: key } }));
|
||||
if (DEBUG) console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
@ -658,8 +674,8 @@ export class DrawingsInitEvent {
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(drawingsData: any, navpointData?: any /*TODO*/) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, {detail: drawingsData}));
|
||||
static dispatch(drawingsData: any, navpointData?: any /*TODO*/) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: drawingsData }));
|
||||
if (DEBUG) console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
@ -722,24 +738,24 @@ export class AudioSinksChangedEvent {
|
||||
}
|
||||
|
||||
export class SRSClientsChangedEvent {
|
||||
static on(callback: () => void, singleShot = false) {
|
||||
static on(callback: (clientsData: SRSClientData[]) => void, singleShot = false) {
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback();
|
||||
callback(ev.detail.clientsData);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch() {
|
||||
document.dispatchEvent(new CustomEvent(this.name));
|
||||
static dispatch(clientsData: SRSClientData[]) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: { clientsData } }));
|
||||
// Logging disabled since periodic
|
||||
}
|
||||
}
|
||||
|
||||
export class AudioManagerStateChangedEvent {
|
||||
static on(callback: (state: boolean) => void, singleShot = false) {
|
||||
static on(callback: (state: string) => void, singleShot = false) {
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
@ -749,7 +765,7 @@ export class AudioManagerStateChangedEvent {
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(state: boolean) {
|
||||
static dispatch(state: string) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: { state } }));
|
||||
if (DEBUG) console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
@ -895,12 +911,9 @@ export class WeaponsRefreshedEvent {
|
||||
|
||||
export class CoordinatesFreezeEvent {
|
||||
static on(callback: () => void) {
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback();
|
||||
}
|
||||
)
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
static dispatch() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { LatLng } from "leaflet";
|
||||
import { Coalition, MapOptions } from "./types/types";
|
||||
import { AudioOptions, Coalition, MapOptions } from "./types/types";
|
||||
|
||||
export interface OlympusConfig {
|
||||
/* Set by user */
|
||||
@ -64,8 +64,9 @@ export interface SessionData {
|
||||
}
|
||||
|
||||
export interface ProfileOptions {
|
||||
mapOptions: MapOptions;
|
||||
shortcuts: { [key: string]: ShortcutOptions };
|
||||
mapOptions?: MapOptions;
|
||||
shortcuts?: { [key: string]: ShortcutOptions };
|
||||
audioOptions?: AudioOptions;
|
||||
}
|
||||
|
||||
export interface ContextMenuOption {
|
||||
|
||||
@ -21,7 +21,7 @@ import { ServerManager } from "./server/servermanager";
|
||||
import { AudioManager } from "./audio/audiomanager";
|
||||
|
||||
import { GAME_MASTER, LoginSubState, MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, WarningSubstate } from "./constants/constants";
|
||||
import { AdminPasswordChangedEvent, AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events";
|
||||
import { AdminPasswordChangedEvent, AppStateChangedEvent, AudioOptionsChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events";
|
||||
import { OlympusConfig } from "./interfaces";
|
||||
import { SessionDataManager } from "./sessiondata";
|
||||
import { ControllerManager } from "./controllers/controllermanager";
|
||||
@ -64,8 +64,9 @@ export class OlympusApp {
|
||||
else this.getState() === OlympusState.UNIT_CONTROL && this.setState(OlympusState.IDLE);
|
||||
});
|
||||
|
||||
MapOptionsChangedEvent.on((options) => getApp().saveProfile());
|
||||
ShortcutsChangedEvent.on((options) => getApp().saveProfile());
|
||||
MapOptionsChangedEvent.on(() => getApp().saveProfile());
|
||||
ShortcutsChangedEvent.on(() => getApp().saveProfile());
|
||||
AudioOptionsChangedEvent.on(() => getApp().saveProfile());
|
||||
}
|
||||
|
||||
getMap() {
|
||||
@ -212,6 +213,7 @@ export class OlympusApp {
|
||||
let profile = {};
|
||||
profile["mapOptions"] = this.#map?.getOptions();
|
||||
profile["shortcuts"] = this.#shortcutManager?.getShortcutsOptions();
|
||||
profile["audioOptions"] = this.#audioManager?.getOptions();
|
||||
|
||||
const requestOptions = {
|
||||
method: "PUT", // Specify the request method
|
||||
@ -285,8 +287,10 @@ export class OlympusApp {
|
||||
const username = this.getServerManager().getUsername();
|
||||
const profile = this.getProfile();
|
||||
if (username && profile) {
|
||||
this.#map?.setOptions( {...MAP_OPTIONS_DEFAULTS, ...profile.mapOptions});
|
||||
this.#shortcutManager?.setShortcutsOptions(profile.shortcuts);
|
||||
if (profile.mapOptions) this.#map?.setOptions( {...MAP_OPTIONS_DEFAULTS, ...profile.mapOptions});
|
||||
if (profile.shortcuts) this.#shortcutManager?.setShortcutsOptions(profile.shortcuts);
|
||||
if (profile.audioOptions) this.#audioManager?.setOptions(profile.audioOptions);
|
||||
|
||||
this.addInfoMessage("Profile loaded correctly");
|
||||
console.log(`Profile for ${username} loaded correctly`);
|
||||
} else {
|
||||
@ -335,6 +339,10 @@ export class OlympusApp {
|
||||
AdminPasswordChangedEvent.dispatch(newAdminPassword);
|
||||
}
|
||||
|
||||
getAdminPassword() {
|
||||
return this.#adminPassword;
|
||||
}
|
||||
|
||||
startServerMode() {
|
||||
//ConfigLoadedEvent.on((config) => {
|
||||
// this.getAudioManager().start();
|
||||
|
||||
@ -35,6 +35,11 @@ export type MapOptions = {
|
||||
showUnitCallsigns: boolean;
|
||||
};
|
||||
|
||||
export type AudioOptions = {
|
||||
input: string;
|
||||
output: string;
|
||||
}
|
||||
|
||||
export type MapHiddenTypes = {
|
||||
human: boolean;
|
||||
olympus: boolean;
|
||||
@ -65,3 +70,22 @@ export type MGRS = {
|
||||
|
||||
export type Coalition = "blue" | "neutral" | "red" | "all";
|
||||
|
||||
export type SRSClientData = {
|
||||
name: string;
|
||||
unitID: number;
|
||||
iff: {
|
||||
control: number;
|
||||
mode1: number;
|
||||
mode2: number;
|
||||
mode3: number;
|
||||
mode4: boolean;
|
||||
mic: number;
|
||||
status: number;
|
||||
},
|
||||
coalition: number;
|
||||
radios: {
|
||||
frequency: number;
|
||||
modulation: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
|
||||
@ -9,8 +9,8 @@ import { AudioSink } from "../../audio/audiosink";
|
||||
import { RadioSink } from "../../audio/radiosink";
|
||||
import { UnitSinkPanel } from "./components/unitsinkpanel";
|
||||
import { UnitSink } from "../../audio/unitsink";
|
||||
import { FaMinus, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { getRandomColor } from "../../other/utils";
|
||||
import { FaMinus, FaPerson, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { enumToCoalition, getRandomColor, zeroAppend } from "../../other/utils";
|
||||
import {
|
||||
AudioManagerCoalitionChangedEvent,
|
||||
AudioManagerDevicesChangedEvent,
|
||||
@ -21,11 +21,13 @@ import {
|
||||
AudioSourcesChangedEvent,
|
||||
CommandModeOptionsChangedEvent,
|
||||
ShortcutsChangedEvent,
|
||||
SRSClientsChangedEvent,
|
||||
} from "../../events";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { GAME_MASTER, NONE } from "../../constants/constants";
|
||||
import { Coalition, SRSClientData } from "../../types/types";
|
||||
import { AudioManagerState, GAME_MASTER, NONE } from "../../constants/constants";
|
||||
import { AudioManager } from "../../audio/audiomanager";
|
||||
|
||||
export function AudioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [devices, setDevices] = useState([] as MediaDeviceInfo[]);
|
||||
@ -39,6 +41,8 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
const [output, setOutput] = useState(undefined as undefined | MediaDeviceInfo);
|
||||
const [coalition, setCoalition] = useState("blue" as Coalition);
|
||||
const [commandMode, setCommandMode] = useState(NONE as string);
|
||||
const [clientsData, setClientsData] = useState([] as SRSClientData[]);
|
||||
const [connectedClientsOpen, setConnectedClientsOpen] = useState(false);
|
||||
|
||||
/* Preallocate 128 references for the source and sink panels. If the number of references changes, React will give an error */
|
||||
const sourceRefs = Array(128)
|
||||
@ -76,7 +80,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
});
|
||||
|
||||
AudioManagerStateChangedEvent.on(() => {
|
||||
setAudioManagerEnabled(getApp().getAudioManager().isRunning());
|
||||
setAudioManagerEnabled(getApp().getAudioManager().isRunning() === AudioManagerState.RUNNING);
|
||||
});
|
||||
|
||||
ShortcutsChangedEvent.on((shortcuts) => setShortcuts(shortcuts));
|
||||
@ -86,6 +90,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
AudioManagerOutputChangedEvent.on((output) => setOutput(output));
|
||||
AudioManagerCoalitionChangedEvent.on((coalition) => setCoalition(coalition));
|
||||
CommandModeOptionsChangedEvent.on((options) => setCommandMode(options.commandMode));
|
||||
SRSClientsChangedEvent.on((clientsData) => setClientsData(clientsData));
|
||||
}, []);
|
||||
|
||||
/* When the sinks or sources change, use the count state to force a rerender to update the connection lines */
|
||||
@ -131,62 +136,78 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
>
|
||||
<h2 className="mb-4 font-bold">Audio menu</h2>
|
||||
<div>
|
||||
The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies using the SRS integration. In other words, it allows you to communicate over SRS without the need of using the SRS client.
|
||||
The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies using the
|
||||
SRS integration. In other words, it allows you to communicate over SRS without the need of using the SRS client.
|
||||
</div>
|
||||
<div>
|
||||
Because of the limitations of the browser, you need to enable the audio backend by clicking on the volume icon in the navigation header. Moreover, you need to allow the browser to access your microphone and speakers. It may take a couple of seconds for the audio backend to start.
|
||||
</div>
|
||||
<div className="text-red-500">
|
||||
For security reasons, the audio backend will only work if the page is served over HTTPS.
|
||||
Because of the limitations of the browser, you need to enable the audio backend by clicking on the volume icon in the navigation header. Moreover,
|
||||
you need to allow the browser to access your microphone and speakers. It may take a couple of seconds for the audio backend to start.
|
||||
</div>
|
||||
<div className="text-red-500">For security reasons, the audio backend will only work if the page is served over HTTPS.</div>
|
||||
<h2 className="my-4 font-bold">Managing the audio backend</h2>
|
||||
<div>
|
||||
You can select the input and output devices for the audio backend. The input device is the microphone that will be used to transmit your voice. The output device is the speaker that will be used to play the audio from the other players.
|
||||
You can select the input and output devices for the audio backend. The input device is the microphone that will be used to transmit your voice.
|
||||
The output device is the speaker that will be used to play the audio from the other players.
|
||||
</div>
|
||||
<div>
|
||||
You can also select the radio coalition. This will determine the default coalition for the radios you create. If you are in command mode, you can change the radio
|
||||
coalition by clicking on the coalition toggle button. This will have no effect if radio coalition enforcing is not enabled in the SRS server.
|
||||
You can also select the radio coalition. This will determine the default coalition for the radios you create. If you are in command mode, you can
|
||||
change the radio coalition by clicking on the coalition toggle button. This will have no effect if radio coalition enforcing is not enabled in the
|
||||
SRS server.
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Creating audio sources</h2>
|
||||
<div>
|
||||
You can add audio sources by clicking on the "Add audio source" button. By default, a microphone and a text to speech source are created, but you can add file sources as well, which allow to play audio files such as music, prerecorded messages, or background noise, such as gunfire or engine sounds.
|
||||
You can add audio sources by clicking on the "Add audio source" button. By default, a microphone and a text to speech source are created, but you
|
||||
can add file sources as well, which allow to play audio files such as music, prerecorded messages, or background noise, such as gunfire or engine
|
||||
sounds.
|
||||
</div>
|
||||
<div>
|
||||
The text to speech generation works using the Google Cloud speech API and by default it works in English. For it to work, a valid Google Cloud API key must be installed on the Olympus backend server machine. See the backend documentation for more information. {/* TODO: put link here */}
|
||||
The text to speech generation works using the Google Cloud speech API and by default it works in English. For it to work, a valid Google Cloud API
|
||||
key must be installed on the Olympus backend server machine. See the backend documentation for more information. {/* TODO: put link here */}
|
||||
</div>
|
||||
<div>
|
||||
Text to speech and file sources can be set to operate in loop mode, which will make them repeat the audio indefinitely. This is useful for background noise or music. Moreover, you can set the volume of the audio sources.
|
||||
Text to speech and file sources can be set to operate in loop mode, which will make them repeat the audio indefinitely. This is useful for
|
||||
background noise or music. Moreover, you can set the volume of the audio sources.
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Creating radios and loudspeakers</h2>
|
||||
<div>
|
||||
By default, two radios are created, but you can add more by clicking on the "Add radio" button. Radios can be tuned to different frequencies, and they can be set to operate in AM or FM mode. You can also set the volume of the radios, and change the balance between the left and right channels.
|
||||
</div>
|
||||
<div>
|
||||
When a new radio is created, it will NOT be in "listen" mode, so you will need to click on the "Tune to radio" button to start listening.
|
||||
By default, two radios are created, but you can add more by clicking on the "Add radio" button. Radios can be tuned to different frequencies, and
|
||||
they can be set to operate in AM or FM mode. You can also set the volume of the radios, and change the balance between the left and right
|
||||
channels.
|
||||
</div>
|
||||
<div>When a new radio is created, it will NOT be in "listen" mode, so you will need to click on the "Tune to radio" button to start listening.</div>
|
||||
<div>
|
||||
You have three options to transmit on the radio:
|
||||
<div>
|
||||
<li>By clicking on the "Talk on frequency" button on the radio panel. This will enable continuous transmission and will remain "on" until clicked again.</li>
|
||||
<li>
|
||||
By clicking on the "Talk on frequency" button on the radio panel. This will enable continuous transmission and will remain "on" until clicked
|
||||
again.
|
||||
</li>
|
||||
<li>By clicking on the "Push to talk" button located over the mouse coordinates panel, on the bottom right corner of the map.</li>
|
||||
<li>By using the "Push to talk" keyboard shortcuts, which can be edited in the options menu.</li>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Loudspeakers can be used to simulate environmental sounds, like 5MC calls on the carrier, or sirens. To create a loudspeaker, click on the unit that should broadcast the sound, and then click on the "Loudspeakers" button. PTT buttons for loudspeakers operate in the same way as radios.
|
||||
Loudspeakers can be used to simulate environmental sounds, like 5MC calls on the carrier, or sirens. To create a loudspeaker, click on the unit
|
||||
that should broadcast the sound, and then click on the "Loudspeakers" button. PTT buttons for loudspeakers operate in the same way as radios.
|
||||
</div>
|
||||
<div className="text-red-500">
|
||||
The loudspeakers system uses the SRS integration, so it will only work if other players' SRS clients are running and connected to the same server as Olympus. Moreover, the loudspeaker system operates using the INTERCOM radio in SRS, and for the time being it will only work for those radios that have the INTERCOM radio enabled (i.e. usually multicrew aircraft).
|
||||
The loudspeakers system uses the SRS integration, so it will only work if other players' SRS clients are running and connected to the same server
|
||||
as Olympus. Moreover, the loudspeaker system operates using the INTERCOM radio in SRS, and for the time being it will only work for those radios
|
||||
that have the INTERCOM radio enabled (i.e. usually multicrew aircraft).
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Connecting sources and radios/loudspeakers</h2>
|
||||
<div>
|
||||
Each source can be connected to one or more radios or loudspeakers. To connect a source to a radio or loudspeaker, click on the "+" button on the right of the source panel, then click on the equivalent button on the desired radio/loudspeaker. To disconnect a source from a radio or loudspeaker, click on the "-" button next to the radio/loudspeaker.
|
||||
Each source can be connected to one or more radios or loudspeakers. To connect a source to a radio or loudspeaker, click on the "+" button on the
|
||||
right of the source panel, then click on the equivalent button on the desired radio/loudspeaker. To disconnect a source from a radio or
|
||||
loudspeaker, click on the "-" button next to the radio/loudspeaker.
|
||||
</div>
|
||||
<div>
|
||||
The connection lines will show the connections between the sources and the radios/loudspeakers. The color of the line is randomly generated and will be different for each source.
|
||||
The connection lines will show the connections between the sources and the radios/loudspeakers. The color of the line is randomly generated and
|
||||
will be different for each source.
|
||||
</div>
|
||||
<div>
|
||||
By connecting multiple sources to the same radio/loudspeaker, you can create complex audio setups, like playing background music while transmitting on the radio.
|
||||
By connecting multiple sources to the same radio/loudspeaker, you can create complex audio setups, like playing background music while
|
||||
transmitting on the radio.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -224,17 +245,78 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<>
|
||||
{audioManagerEnabled && (
|
||||
<>
|
||||
<div className={`mb-4 flex flex-col content-center bg-olympus-500`}>
|
||||
<div
|
||||
className={`
|
||||
flex cursor-pointer content-center gap-2 px-5 py-2
|
||||
text-gray-200
|
||||
hover:underline hover:underline-offset-4
|
||||
hover:underline-gray-700
|
||||
`}
|
||||
onClick={() => setConnectedClientsOpen(!connectedClientsOpen)}
|
||||
>
|
||||
Connected clients <FaPerson className="my-auto ml-auto" /> <div className={``}>{clientsData.length}</div>
|
||||
<svg
|
||||
data-open={connectedClientsOpen}
|
||||
className={`
|
||||
my-auto h-3 w-3 shrink-0 -rotate-180 transition-transform
|
||||
dark:text-olympus-50
|
||||
data-[open='false']:-rotate-90
|
||||
`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 10 6"
|
||||
>
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5 5 1 1 5" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 px-5 font-normal text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
style={{ paddingRight: `${paddingRight}px` }}
|
||||
>
|
||||
{audioManagerEnabled && (
|
||||
<>
|
||||
{connectedClientsOpen && (
|
||||
<div className={`flex flex-col text-gray-200`}>
|
||||
{clientsData.map((clientData, idx) => {
|
||||
return (
|
||||
<div
|
||||
data-coalition={enumToCoalition(clientData.coalition)}
|
||||
key={idx}
|
||||
className={`
|
||||
flex gap-2 border-l-4 px-4 py-2
|
||||
data-[coalition='blue']:border-blue-500
|
||||
data-[coalition='neutral']:border-gray-500
|
||||
data-[coalition='red']:border-red-500
|
||||
`}
|
||||
>
|
||||
<div className="text-gray-400">{clientData.name}</div>
|
||||
<div
|
||||
className={`
|
||||
ml-auto cursor-pointer gap-2 rounded-md
|
||||
bg-olympus-600 px-3 py-1 text-sm
|
||||
hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getAudioManager().tuneNewRadio(clientData.radios[0].frequency, clientData.radios[0].modulation)}
|
||||
>
|
||||
{`${zeroAppend(clientData.radios[0].frequency / 1e6, 3, true, 3)} ${clientData.radios[0].modulation ? "FM" : "AM"}`}{" "}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
cursor-pointer gap-2 rounded-md bg-olympus-600 px-3
|
||||
py-1 text-sm
|
||||
hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getAudioManager().tuneNewRadio(clientData.radios[1].frequency, clientData.radios[1].modulation)}
|
||||
>
|
||||
{`${zeroAppend(clientData.radios[1].frequency / 1e6, 3, true, 3)} ${clientData.radios[1].modulation ? "FM" : "AM"}`}{" "}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="my-4 flex flex-col gap-2 px-5 text-gray-200">
|
||||
{commandMode === GAME_MASTER && (
|
||||
<div className="flex justify-between">
|
||||
<div>Radio coalition </div>
|
||||
@ -254,23 +336,18 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
)}
|
||||
|
||||
<span>Input</span>
|
||||
|
||||
<OlDropdown label={input ? input.label : "Default"}>
|
||||
{devices
|
||||
.filter((device) => device.kind === "audioinput")
|
||||
.map((device, idx) => {
|
||||
return (
|
||||
<OlDropdownItem onClick={() => getApp().getAudioManager().setInput(device)}>
|
||||
<div className="w-full truncate">{device.label}</div>
|
||||
<div className="w-full truncate text-left">{device.label}</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</>
|
||||
)}
|
||||
{audioManagerEnabled && (
|
||||
<>
|
||||
{" "}
|
||||
|
||||
<span>Output</span>
|
||||
<OlDropdown label={output ? output.label : "Default"}>
|
||||
{devices
|
||||
@ -278,13 +355,24 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
.map((device, idx) => {
|
||||
return (
|
||||
<OlDropdownItem onClick={() => getApp().getAudioManager().setOutput(device)}>
|
||||
<div className="w-full truncate">{device.label}</div>
|
||||
<div className="w-full truncate text-left">{device.label}</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 px-5 font-normal text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
style={{ paddingRight: `${paddingRight}px` }}
|
||||
>
|
||||
{audioManagerEnabled && <span>Audio sources</span>}
|
||||
<>
|
||||
{sources.map((source, idx) => {
|
||||
@ -328,7 +416,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
if (sink instanceof RadioSink)
|
||||
return (
|
||||
<RadioSinkPanel
|
||||
shortcutKeys={shortcuts[`PTT${idx}Active`].toActions()}
|
||||
shortcutKeys={shortcuts[`PTT${idx}Active`]?.toActions() ?? []}
|
||||
key={sink.getName()}
|
||||
radio={sink}
|
||||
onExpanded={() => {
|
||||
@ -366,7 +454,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
if (sink instanceof UnitSink)
|
||||
return (
|
||||
<UnitSinkPanel
|
||||
shortcutKeys={shortcuts[`PTT${idx}Active`].toActions()}
|
||||
shortcutKeys={shortcuts[`PTT${idx}Active`]?.toActions() ?? []}
|
||||
key={sink.getName()}
|
||||
sink={sink}
|
||||
ref={sinkRefs[idx]}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import React, { ForwardedRef, forwardRef, useEffect, useState } from "react";
|
||||
import { OlFrequencyInput } from "../../components/olfrequencyinput";
|
||||
import { FaChevronUp, FaVolumeHigh, FaXmark } from "react-icons/fa6";
|
||||
import { FaChevronDown, FaChevronUp, FaPerson, FaVolumeHigh, FaXmark } from "react-icons/fa6";
|
||||
import { OlLabelToggle } from "../../components/ollabeltoggle";
|
||||
import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
|
||||
import { RadioSink } from "../../../audio/radiosink";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
import { OlRangeSlider } from "../../components/olrangeslider";
|
||||
import { zeroAppend } from "../../../other/utils";
|
||||
|
||||
export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKeys: string[]; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
@ -24,21 +25,23 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
|
||||
data-[receiving='true']:border-white
|
||||
`}
|
||||
ref={ref}
|
||||
|
||||
>
|
||||
<div className="flex cursor-pointer content-center justify-between gap-2" onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}>
|
||||
<div
|
||||
className="flex cursor-pointer content-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`h-fit w-fit cursor-pointer rounded-sm py-2`}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<FaChevronUp
|
||||
<FaChevronDown
|
||||
className={`
|
||||
text-gray-500 transition-transform
|
||||
data-[expanded='false']:rotate-180
|
||||
data-[expanded='false']:-rotate-90
|
||||
`}
|
||||
data-expanded={expanded}
|
||||
/>
|
||||
@ -56,14 +59,18 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
|
||||
</kbd>
|
||||
</>
|
||||
)}
|
||||
<span className="my-auto w-full">{!expanded && `${props.radio.getFrequency() / 1e6} MHz ${props.radio.getModulation() ? "FM" : "AM"}`}</span>
|
||||
<div className="my-auto flex w-full">
|
||||
{!expanded && `${zeroAppend(props.radio.getFrequency() / 1e6, 3, true, 3)} ${props.radio.getModulation() ? "FM" : "AM"}`}
|
||||
<FaPerson className="my-auto ml-auto" /> {props.radio.getConnectedClients()}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
mb-auto ml-auto aspect-square cursor-pointer rounded-md p-2
|
||||
hover:bg-white/10
|
||||
`}
|
||||
onClick={() => {
|
||||
onClick={(ev) => {
|
||||
getApp().getAudioManager().removeSink(props.radio);
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<FaXmark className={`my-auto text-gray-500`}></FaXmark>
|
||||
@ -71,14 +78,18 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
|
||||
</div>
|
||||
{expanded && (
|
||||
<>
|
||||
<OlFrequencyInput
|
||||
value={props.radio.getFrequency()}
|
||||
onChange={(value) => {
|
||||
props.radio.setFrequency(value);
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2 flex w-full justify-center">
|
||||
<OlFrequencyInput
|
||||
value={props.radio.getFrequency()}
|
||||
onChange={(value) => {
|
||||
props.radio.setFrequency(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex content-center gap-2 p-2">
|
||||
<div><FaVolumeHigh className="text-xl"/></div>
|
||||
<div>
|
||||
<FaVolumeHigh className="text-xl" />
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
value={props.radio.getVolume() * 100}
|
||||
onChange={(ev) => {
|
||||
@ -89,14 +100,14 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
|
||||
</div>
|
||||
<div className="flex content-center gap-2 p-2">
|
||||
<div>Left</div>
|
||||
<OlRangeSlider
|
||||
value={props.radio.getPan() * 50 + 50}
|
||||
onChange={(ev) => {
|
||||
props.radio.setPan((Number(ev.currentTarget.value) - 50) / 50);
|
||||
}}
|
||||
className="my-auto"
|
||||
></OlRangeSlider>
|
||||
<div>Right</div>
|
||||
<OlRangeSlider
|
||||
value={props.radio.getPan() * 50 + 50}
|
||||
onChange={(ev) => {
|
||||
props.radio.setPan((Number(ev.currentTarget.value) - 50) / 50);
|
||||
}}
|
||||
className="my-auto"
|
||||
></OlRangeSlider>
|
||||
<div>Right</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<OlLabelToggle
|
||||
|
||||
@ -3,7 +3,7 @@ import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faHourglass, faPause, faPlay, faRepeat, faStop } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
import { AudioSource } from "../../../audio/audiosource";
|
||||
import { FaChevronUp, FaVolumeHigh, FaXmark } from "react-icons/fa6";
|
||||
import { FaChevronDown, FaChevronUp, FaVolumeHigh, FaXmark } from "react-icons/fa6";
|
||||
import { OlRangeSlider } from "../../components/olrangeslider";
|
||||
import { FileSource } from "../../../audio/filesource";
|
||||
import { MicrophoneSource } from "../../../audio/microphonesource";
|
||||
@ -40,10 +40,10 @@ export const AudioSourcePanel = forwardRef((props: { source: AudioSource; onExpa
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<FaChevronUp
|
||||
<FaChevronDown
|
||||
className={`
|
||||
text-gray-500 transition-transform
|
||||
data-[expanded='false']:rotate-180
|
||||
data-[expanded='false']:-rotate-90
|
||||
`}
|
||||
data-expanded={expanded}
|
||||
/>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import React, { ForwardedRef, forwardRef, useEffect, useState } from "react";
|
||||
import { FaChevronUp, FaXmark } from "react-icons/fa6";
|
||||
import { FaChevronDown, FaChevronUp, FaVolumeHigh, FaXmark } from "react-icons/fa6";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
import { UnitSink } from "../../../audio/unitsink";
|
||||
import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faMicrophoneLines, faVolumeHigh } from "@fortawesome/free-solid-svg-icons";
|
||||
import { OlRangeSlider } from "../../components/olrangeslider";
|
||||
|
||||
export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKeys: string[]; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
@ -28,10 +28,10 @@ export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKeys:
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<FaChevronUp
|
||||
<FaChevronDown
|
||||
className={`
|
||||
text-gray-500 transition-transform
|
||||
data-[expanded='false']:rotate-180
|
||||
data-[expanded='false']:-rotate-90
|
||||
`}
|
||||
data-expanded={expanded}
|
||||
/>
|
||||
@ -66,7 +66,7 @@ export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKeys:
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<span className="my-auto">Near</span>
|
||||
<FaVolumeHigh className="my-auto w-8 text-xl" />
|
||||
<OlRangeSlider
|
||||
value={((props.sink.getMaxDistance() - 100) / (1852 - 100)) * 100}
|
||||
min={0}
|
||||
@ -76,14 +76,13 @@ export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKeys:
|
||||
}}
|
||||
className="my-auto h-16"
|
||||
/>
|
||||
<span className="my-auto">Far</span>
|
||||
<OlStateButton
|
||||
checked={props.sink.getPtt()}
|
||||
icon={faMicrophoneLines}
|
||||
icon={faVolumeHigh}
|
||||
onClick={() => {
|
||||
props.sink.setPtt(!props.sink.getPtt());
|
||||
}}
|
||||
tooltip="Talk on frequency"
|
||||
tooltip="Click to enable the loudspeaker"
|
||||
></OlStateButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -91,14 +91,6 @@ export function ControlsPanel(props: {}) {
|
||||
target: faFighterJet,
|
||||
text: "Show unit actions",
|
||||
});
|
||||
//controls.push({
|
||||
// actions: shortcuts["toggleRelativePositions"]?.toActions(),
|
||||
// text: "Activate group movement",
|
||||
//});
|
||||
//controls.push({
|
||||
// actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
|
||||
// text: "Rotate formation",
|
||||
//});
|
||||
} else if (appState === OlympusState.SPAWN) {
|
||||
controls = [
|
||||
{
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
} from "../components/olicons";
|
||||
import { FaChevronLeft, FaChevronRight, FaFloppyDisk } from "react-icons/fa6";
|
||||
import {
|
||||
AudioManagerStateChangedEvent,
|
||||
CommandModeOptionsChangedEvent,
|
||||
ConfigLoadedEvent,
|
||||
EnabledCommandModesChangedEvent,
|
||||
@ -27,6 +28,7 @@ import {
|
||||
SessionDataSavedEvent,
|
||||
} from "../../events";
|
||||
import {
|
||||
AudioManagerState,
|
||||
BLUE_COMMANDER,
|
||||
COMMAND_MODE_OPTIONS_DEFAULTS,
|
||||
GAME_MASTER,
|
||||
@ -39,6 +41,7 @@ import {
|
||||
import { OlympusConfig } from "../../interfaces";
|
||||
import { FaCheck, FaRedo, FaSpinner } from "react-icons/fa";
|
||||
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
|
||||
import { stat } from "fs";
|
||||
|
||||
export function Header() {
|
||||
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
|
||||
@ -47,7 +50,7 @@ export function Header() {
|
||||
const [mapSources, setMapSources] = useState([] as string[]);
|
||||
const [scrolledLeft, setScrolledLeft] = useState(true);
|
||||
const [scrolledRight, setScrolledRight] = useState(false);
|
||||
const [audioEnabled, setAudioEnabled] = useState(false);
|
||||
const [audioState, setAudioState] = useState(AudioManagerState.STOPPED);
|
||||
const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS);
|
||||
const [savingSessionData, setSavingSessionData] = useState(false);
|
||||
const [latestVersion, setLatestVersion] = useState("");
|
||||
@ -77,6 +80,7 @@ export function Header() {
|
||||
SessionDataChangedEvent.on(() => setSavingSessionData(true));
|
||||
SessionDataSavedEvent.on(() => setSavingSessionData(false));
|
||||
EnabledCommandModesChangedEvent.on((enabledCommandModes) => setEnabledCommandModes(enabledCommandModes));
|
||||
AudioManagerStateChangedEvent.on((state) => setAudioState(state as AudioManagerState));
|
||||
|
||||
/* Check if we are running the latest version */
|
||||
const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json");
|
||||
@ -246,9 +250,15 @@ export function Header() {
|
||||
>
|
||||
<span className="my-auto font-bold">Game Master</span>
|
||||
{enabledCommandModes.length > 0 && (
|
||||
<>{loadingNewCommandMode ? <FaSpinner className={`
|
||||
my-auto ml-2 animate-spin text-white
|
||||
`} /> : <FaRedo className={`my-auto ml-2 text-gray-200`} />}</>
|
||||
<>
|
||||
{loadingNewCommandMode ? (
|
||||
<FaSpinner
|
||||
className={`my-auto ml-2 animate-spin text-white`}
|
||||
/>
|
||||
) : (
|
||||
<FaRedo className={`my-auto ml-2 text-gray-200`} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -271,9 +281,15 @@ export function Header() {
|
||||
>
|
||||
<span className="my-auto font-bold">BLUE Commander</span>
|
||||
{enabledCommandModes.length > 0 && (
|
||||
<>{loadingNewCommandMode ? <FaSpinner className={`
|
||||
my-auto ml-2 animate-spin text-gray-200
|
||||
`} /> : <FaRedo className={`my-auto ml-2 text-gray-200`} />}</>
|
||||
<>
|
||||
{loadingNewCommandMode ? (
|
||||
<FaSpinner
|
||||
className={`my-auto ml-2 animate-spin text-gray-200`}
|
||||
/>
|
||||
) : (
|
||||
<FaRedo className={`my-auto ml-2 text-gray-200`} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -296,9 +312,15 @@ export function Header() {
|
||||
>
|
||||
<span className="my-auto font-bold">RED Commander</span>
|
||||
{enabledCommandModes.length > 0 && (
|
||||
<>{loadingNewCommandMode ? <FaSpinner className={`
|
||||
my-auto ml-2 animate-spin text-gray-200
|
||||
`} /> : <FaRedo className={`my-auto ml-2 text-gray-200`} />}</>
|
||||
<>
|
||||
{loadingNewCommandMode ? (
|
||||
<FaSpinner
|
||||
className={`my-auto ml-2 animate-spin text-gray-200`}
|
||||
/>
|
||||
) : (
|
||||
<FaRedo className={`my-auto ml-2 text-gray-200`} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -325,11 +347,13 @@ export function Header() {
|
||||
)}
|
||||
/>
|
||||
<OlRoundStateButton
|
||||
checked={audioEnabled}
|
||||
checked={audioState === AudioManagerState.RUNNING}
|
||||
onClick={() => {
|
||||
audioEnabled ? getApp().getAudioManager().stop() : getApp().getAudioManager().start();
|
||||
setAudioEnabled(!audioEnabled);
|
||||
audioState === AudioManagerState.RUNNING ? getApp().getAudioManager().stop() : getApp().getAudioManager().start();
|
||||
}}
|
||||
className={audioState === AudioManagerState.ERROR ? `
|
||||
animate-pulse !border-red-500 !text-red-500
|
||||
` : ""}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Enable/disable audio"
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, InfoPopupEvent } from "../../events";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { ContextAction } from "../../unit/contextaction";
|
||||
import { InfoPopupEvent } from "../../events";
|
||||
|
||||
|
||||
export function InfoBar(props: {}) {
|
||||
const [messages, setMessages] = useState([] as string[]);
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [contextAction, setContextAction] = useState(null as ContextAction | null);
|
||||
|
||||
useEffect(() => {
|
||||
InfoPopupEvent.on((messages) => setMessages([...messages]));
|
||||
AppStateChangedEvent.on((state, subState) => setAppState(state));
|
||||
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
SelectionEnabledChangedEvent,
|
||||
ShortcutsChangedEvent,
|
||||
} from "../../events";
|
||||
import { faCopy, faEraser, faObjectGroup, faPaste, faTape } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCopy, faEraser, faMinus, faObjectGroup, faPaste, faPlus, faTape } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Shortcut } from "../../shortcut/shortcut";
|
||||
import { ShortcutOptions, UnitData } from "../../interfaces";
|
||||
import { Unit } from "../../unit/unit";
|
||||
@ -146,12 +146,36 @@ export function MapToolBar(props: {}) {
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
<div className={`
|
||||
pointer-events-auto flex flex-col gap-2 overflow-y-auto no-scrollbar
|
||||
p-2
|
||||
`} onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
|
||||
<div
|
||||
className={`
|
||||
pointer-events-auto flex flex-col gap-2 overflow-y-auto
|
||||
no-scrollbar p-2
|
||||
`}
|
||||
onScroll={(ev) => onScroll(ev.target)}
|
||||
ref={scrollRef}
|
||||
>
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<OlStateButton
|
||||
key={"select"}
|
||||
checked={false}
|
||||
icon={faPlus}
|
||||
tooltip={() => <div>Zoom map in</div>}
|
||||
tooltipPosition="side"
|
||||
onClick={() => {
|
||||
getApp().getMap().zoomIn();
|
||||
}}
|
||||
/>
|
||||
<OlStateButton
|
||||
key={"select"}
|
||||
checked={false}
|
||||
icon={faMinus}
|
||||
tooltip={() => <div>Zoom map out</div>}
|
||||
tooltipPosition="side"
|
||||
onClick={() => {
|
||||
getApp().getMap().zoomOut();
|
||||
}}
|
||||
/>
|
||||
<OlStateButton
|
||||
key={"select"}
|
||||
checked={selectionEnabled}
|
||||
@ -220,39 +244,39 @@ export function MapToolBar(props: {}) {
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<OlStateButton
|
||||
key={"measure"}
|
||||
checked={appState === OlympusState.MEASURE}
|
||||
icon={faTape}
|
||||
tooltip={() => (
|
||||
<div className="flex content-center gap-2">
|
||||
{shortcutCombination(shortcuts["measure"]?.getOptions())}
|
||||
<div className="my-auto">Enter measure mode</div>
|
||||
</div>
|
||||
)}
|
||||
tooltipPosition="side"
|
||||
onClick={() => {
|
||||
getApp().setState(appState === OlympusState.MEASURE? OlympusState.IDLE : OlympusState.MEASURE);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<OlStateButton
|
||||
key={"clearMeasures"}
|
||||
checked={false}
|
||||
icon={faEraser}
|
||||
tooltip={() => (
|
||||
<div className="flex content-center gap-2">
|
||||
{shortcutCombination(shortcuts["clearMeasures"]?.getOptions())}
|
||||
<div className="my-auto">Clear all measures</div>
|
||||
</div>
|
||||
)}
|
||||
tooltipPosition="side"
|
||||
onClick={() => {
|
||||
getApp().getMap().clearMeasures();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<OlStateButton
|
||||
key={"measure"}
|
||||
checked={appState === OlympusState.MEASURE}
|
||||
icon={faTape}
|
||||
tooltip={() => (
|
||||
<div className="flex content-center gap-2">
|
||||
{shortcutCombination(shortcuts["measure"]?.getOptions())}
|
||||
<div className="my-auto">Enter measure mode</div>
|
||||
</div>
|
||||
)}
|
||||
tooltipPosition="side"
|
||||
onClick={() => {
|
||||
getApp().setState(appState === OlympusState.MEASURE ? OlympusState.IDLE : OlympusState.MEASURE);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<OlStateButton
|
||||
key={"clearMeasures"}
|
||||
checked={false}
|
||||
icon={faEraser}
|
||||
tooltip={() => (
|
||||
<div className="flex content-center gap-2">
|
||||
{shortcutCombination(shortcuts["clearMeasures"]?.getOptions())}
|
||||
<div className="my-auto">Clear all measures</div>
|
||||
</div>
|
||||
)}
|
||||
tooltipPosition="side"
|
||||
onClick={() => {
|
||||
getApp().getMap().clearMeasures();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{reorderedActions.map((contextActionIt: ContextAction) => {
|
||||
|
||||
@ -57,7 +57,7 @@ export function MiniMapPanel(props: {}) {
|
||||
absolute right-[10px]
|
||||
${mapOptions.showMinimap ? `bottom-[188px]` : `bottom-[20px]`}
|
||||
flex w-[288px] cursor-pointer flex-col items-center justify-between
|
||||
gap-2 text-sm backdrop-blur-lg
|
||||
gap-2 text-sm
|
||||
`}
|
||||
|
||||
>
|
||||
@ -65,7 +65,7 @@ export function MiniMapPanel(props: {}) {
|
||||
<CoordinatesPanel />
|
||||
<div className={`
|
||||
flex h-12 w-full items-center justify-between gap-2 px-3
|
||||
backdrop-grayscale
|
||||
backdrop-blur-lg backdrop-grayscale
|
||||
dark:bg-olympus-800/90 dark:text-gray-200
|
||||
${mapOptions.showMinimap ? `rounded-t-lg` : `rounded-lg`}
|
||||
`}
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { AudioSinksChangedEvent } from "../../events";
|
||||
import { AudioSink } from "../../audio/audiosink";
|
||||
import { RadioSink } from "../../audio/radiosink";
|
||||
import { FaJetFighter, FaRadio, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { FaRadio, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { UnitSink } from "../../audio/unitsink";
|
||||
import { colors } from "../../constants/constants";
|
||||
@ -18,12 +18,9 @@ export function RadiosSummaryPanel(props: {}) {
|
||||
<>
|
||||
{audioSinks.length > 0 && (
|
||||
<div
|
||||
className={`
|
||||
flex w-full gap-2 rounded-lg text-sm text-gray-200
|
||||
`}
|
||||
className={`flex w-full gap-2 rounded-lg text-sm text-gray-200`}
|
||||
>
|
||||
<div className="flex w-full flex-wrap gap-2">
|
||||
|
||||
{audioSinks.filter((audioSinks) => audioSinks instanceof RadioSink).length > 0 &&
|
||||
audioSinks
|
||||
.filter((audioSinks) => audioSinks instanceof RadioSink)
|
||||
@ -43,9 +40,12 @@ export function RadiosSummaryPanel(props: {}) {
|
||||
buttonColor={radioSink.getReceiving() ? colors.WHITE : undefined}
|
||||
className="min-h-12 min-w-12"
|
||||
>
|
||||
<span className={`text-gray-200`}><FaRadio className={`
|
||||
-translate-x-2 translate-y-1
|
||||
`} /> <div className="translate-x-2 font-bold">{idx + 1}</div></span>
|
||||
<span className={`text-gray-200`}>
|
||||
<FaRadio
|
||||
className={`-translate-x-2 translate-y-1`}
|
||||
/>{" "}
|
||||
<div className="translate-x-2 font-bold">{idx + 1}</div>
|
||||
</span>
|
||||
</OlStateButton>
|
||||
);
|
||||
})}
|
||||
@ -67,9 +67,12 @@ export function RadiosSummaryPanel(props: {}) {
|
||||
tooltip="Click to talk"
|
||||
className="min-h-12 min-w-12"
|
||||
>
|
||||
<span className={`text-gray-200`}><FaVolumeHigh className={`
|
||||
-translate-x-2 translate-y-1
|
||||
`} /> <div className="translate-x-2 font-bold">{idx + 1}</div></span>
|
||||
<span className={`text-gray-200`}>
|
||||
<FaVolumeHigh
|
||||
className={`-translate-x-2 translate-y-1`}
|
||||
/>{" "}
|
||||
<div className="translate-x-2 font-bold">{idx + 1}</div>
|
||||
</span>
|
||||
</OlStateButton>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -7,6 +7,7 @@ import { getApp } from "../../olympusapp";
|
||||
import { OlButtonGroup, OlButtonGroupItem } from "../components/olbuttongroup";
|
||||
import { OlCheckbox } from "../components/olcheckbox";
|
||||
import {
|
||||
AudioManagerState,
|
||||
ROEs,
|
||||
UnitState,
|
||||
altitudeIncrements,
|
||||
@ -95,7 +96,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
}
|
||||
|
||||
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
|
||||
const [audioManagerState, setAudioManagerState] = useState(false);
|
||||
const [audioManagerRunning, setAudioManagerRunning] = useState(false);
|
||||
const [selectedUnitsData, setSelectedUnitsData] = useState(initializeUnitsData);
|
||||
const [forcedUnitsData, setForcedUnitsData] = useState(initializeUnitsData);
|
||||
const [selectionFilter, setSelectionFilter] = useState({
|
||||
@ -152,7 +153,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
useEffect(() => {
|
||||
SelectedUnitsChangedEvent.on((units) => setSelectedUnits(units));
|
||||
SelectionClearedEvent.on(() => setSelectedUnits([]));
|
||||
AudioManagerStateChangedEvent.on((state) => setAudioManagerState(state));
|
||||
AudioManagerStateChangedEvent.on((state) => setAudioManagerRunning(state === AudioManagerState.RUNNING));
|
||||
UnitsUpdatedEvent.on((units) => units.find((unit) => unit.getSelected()) && setLastUpdateTime(Date.now()));
|
||||
}, []);
|
||||
|
||||
@ -210,8 +211,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
if (newDatum === forcedUnitsData[key]) {
|
||||
anyForcedDataUpdated = true;
|
||||
forcedUnitsData[key] = undefined;
|
||||
}
|
||||
else updatedData[key] = forcedUnitsData[key];
|
||||
} else updatedData[key] = forcedUnitsData[key];
|
||||
} else updatedData[key] = newDatum;
|
||||
});
|
||||
setSelectedUnitsData(updatedData as typeof selectedUnitsData);
|
||||
@ -420,9 +420,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
return (
|
||||
<tr key={idx}>
|
||||
<td className="flex gap-2 text-lg text-gray-200">
|
||||
<FontAwesomeIcon icon={entry[1][0] as IconDefinition} /> <div className={`
|
||||
text-sm text-gray-400
|
||||
`}>{entry[1][1] as string}</div>
|
||||
<FontAwesomeIcon icon={entry[1][0] as IconDefinition} />{" "}
|
||||
<div
|
||||
className={`text-sm text-gray-400`}
|
||||
>
|
||||
{entry[1][1] as string}
|
||||
</div>
|
||||
</td>
|
||||
{["blue", "neutral", "red"].map((coalition) => {
|
||||
return (
|
||||
@ -797,53 +800,65 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeHold} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Hold fire: The unit will not shoot in
|
||||
any circumstance
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeHold}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
Hold fire: The unit will not shoot in any circumstance
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Return fire: The unit will not fire
|
||||
unless fired upon
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeReturn}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
Return fire: The unit will not fire unless fired upon
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} />{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeDesignated}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
<div>
|
||||
{" "}
|
||||
Fire on target: The unit will not fire unless fired upon <p className={`
|
||||
inline font-bold
|
||||
`}>or</p> ordered to do so{" "}
|
||||
Fire on target: The unit will not fire unless fired upon{" "}
|
||||
<p
|
||||
className={`inline font-bold`}
|
||||
>
|
||||
or
|
||||
</p>{" "}
|
||||
ordered to do so{" "}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeFree} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Free: The unit will fire at any
|
||||
detected enemy in range
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeFree}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
Free: The unit will fire at any detected enemy in range
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="my-auto">
|
||||
<FaExclamationCircle className={`
|
||||
animate-bounce text-xl
|
||||
`} />
|
||||
<FaExclamationCircle
|
||||
className={`animate-bounce text-xl`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Currently, DCS blue and red ground units do not respect{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
|
||||
my-auto text-white
|
||||
`} /> and{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
|
||||
my-auto text-white
|
||||
`} /> rules of engagement, so be careful, they
|
||||
may start shooting when you don't want them to. Use neutral units for finer control.
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeReturn}
|
||||
className={`my-auto text-white`}
|
||||
/>{" "}
|
||||
and{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeDesignated}
|
||||
className={`my-auto text-white`}
|
||||
/>{" "}
|
||||
rules of engagement, so be careful, they may start shooting when you don't want them to. Use neutral units for finer
|
||||
control.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -897,35 +912,25 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsAlarmstateGreen}
|
||||
className={`
|
||||
<FontAwesomeIcon icon={olButtonsAlarmstateGreen} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`}
|
||||
/>{" "}
|
||||
Green: The unit will not engage with its sensors in any circumstances. The unit will be able to move.
|
||||
`} /> Green: The unit will not engage
|
||||
with its sensors in any circumstances. The unit will be able to move.
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsAlarmstateAuto}
|
||||
className={`
|
||||
<FontAwesomeIcon icon={olButtonsAlarmstateAuto} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`}
|
||||
/>{" "}
|
||||
`} />{" "}
|
||||
<div> Auto: The unit will use its sensors to engage based on its ROE.</div>
|
||||
</div>
|
||||
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsAlarmstateRed}
|
||||
className={`
|
||||
<FontAwesomeIcon icon={olButtonsAlarmstateRed} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`}
|
||||
/>{" "}
|
||||
Red: The unit will be actively searching for target with its sensors. For some units, this will deploy the radar and make
|
||||
the unit not able to move.
|
||||
`} /> Red: The unit will be actively
|
||||
searching for target with its sensors. For some units, this will deploy the radar and make the unit not able to move.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -982,31 +987,35 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatNone} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> No reaction: The unit will not
|
||||
react in any circumstance
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsThreatNone}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
No reaction: The unit will not react in any circumstance
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatPassive} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Passive: The unit will use
|
||||
counter-measures, but will not alter its course
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsThreatPassive}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
Passive: The unit will use counter-measures, but will not alter its course
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatManoeuvre} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Manouevre: The unit will try
|
||||
to evade the threat using manoeuvres, but no counter-measures
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsThreatManoeuvre}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
Manouevre: The unit will try to evade the threat using manoeuvres, but no counter-measures
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatEvade} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Full evasion: the unit will try
|
||||
to evade the threat both manoeuvering and using counter-measures
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsThreatEvade}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
Full evasion: the unit will try to evade the threat both manoeuvering and using counter-measures
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1057,31 +1066,35 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsSilent} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Radio silence: No radar or
|
||||
ECM will be used
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsEmissionsSilent}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
Radio silence: No radar or ECM will be used
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsDefend} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Defensive: The unit will turn
|
||||
radar and ECM on only when threatened
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsEmissionsDefend}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
Defensive: The unit will turn radar and ECM on only when threatened
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsAttack} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Attack: The unit will use
|
||||
radar and ECM when engaging other units
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsEmissionsAttack}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
Attack: The unit will use radar and ECM when engaging other units
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsFree} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Free: the unit will use the
|
||||
radar and ECM all the time
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsEmissionsFree}
|
||||
className={`my-auto min-w-8 text-white`}
|
||||
/>{" "}
|
||||
Free: the unit will use the radar and ECM all the time
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1299,9 +1312,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div className="my-auto">
|
||||
<FaExclamationCircle className={`
|
||||
animate-bounce text-xl
|
||||
`} />
|
||||
<FaExclamationCircle
|
||||
className={`animate-bounce text-xl`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Currently, DCS blue and red ground units do not respect their rules of engagement, so be careful, they may start shooting when
|
||||
@ -1401,7 +1414,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
{/* ============== Miss on purpose toggle END ============== */}
|
||||
<div className="flex gap-4">
|
||||
{/* ============== Shots scatter START ============== */}
|
||||
<div className={`flex w-full justify-between gap-2`}>
|
||||
<div
|
||||
className={`flex w-full justify-between gap-2`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
@ -1476,9 +1491,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
</div>
|
||||
{/* ============== Operate as toggle START ============== */}
|
||||
{selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (
|
||||
<div className={`
|
||||
flex content-center justify-between
|
||||
`}>
|
||||
<div
|
||||
className={`flex content-center justify-between`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
@ -1814,67 +1829,68 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
</>
|
||||
)}
|
||||
{/* ============== Audio sink toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Loudspeakers
|
||||
</span>
|
||||
{audioManagerState ? (
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.isAudioSink}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
if (!selectedUnitsData.isAudioSink) {
|
||||
getApp()?.getAudioManager().addUnitSink(unit);
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isAudioSink: true,
|
||||
});
|
||||
} else {
|
||||
let sink = getApp()
|
||||
?.getAudioManager()
|
||||
.getSinks()
|
||||
.find((sink) => {
|
||||
return sink instanceof UnitSink && sink.getUnit() === unit;
|
||||
{selectedUnits.length === 1 && (
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Loudspeakers
|
||||
</span>
|
||||
{audioManagerRunning ? (
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.isAudioSink}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
if (!selectedUnitsData.isAudioSink) {
|
||||
getApp()?.getAudioManager().addUnitSink(unit);
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isAudioSink: true,
|
||||
});
|
||||
if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink);
|
||||
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isAudioSink: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Make the unit emit sounds"
|
||||
content="This option allows the unit to emit sounds as if it had loudspeakers. Turn this on to enable the option, then open the audio menu to connect a sound source to the unit. This is useful to simulate 5MC calls on the carrier, or attach sirens to unit. "
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
tooltipPosition="above"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white">
|
||||
Enable audio with{" "}
|
||||
<span
|
||||
className={`
|
||||
mx-1 mt-[-7px] inline-block translate-y-2
|
||||
rounded-full border-[1px] border-white p-1
|
||||
`}
|
||||
>
|
||||
<FaVolumeHigh />
|
||||
</span>{" "}
|
||||
first
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
} else {
|
||||
let sink = getApp()
|
||||
?.getAudioManager()
|
||||
.getSinks()
|
||||
.find((sink) => {
|
||||
return sink instanceof UnitSink && sink.getUnit() === unit;
|
||||
});
|
||||
if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink);
|
||||
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isAudioSink: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Make the unit emit sounds"
|
||||
content="This option allows the unit to emit sounds as if it had loudspeakers. Turn this on to enable the option, then open the audio menu to connect a sound source to the unit. This is useful to simulate 5MC calls on the carrier, or attach sirens to unit. "
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
tooltipPosition="above"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white">
|
||||
Enable audio with{" "}
|
||||
<span
|
||||
className={`
|
||||
mx-1 mt-[-7px] inline-block translate-y-2
|
||||
rounded-full border-[1px] border-white p-1
|
||||
`}
|
||||
>
|
||||
<FaVolumeHigh />
|
||||
</span>{" "}
|
||||
first
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* ============== Audio sink toggle END ============== */}
|
||||
</div>
|
||||
)}
|
||||
@ -1973,9 +1989,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
value={activeRadioSettings ? activeRadioSettings.TACAN.channel : 1}
|
||||
></OlNumberInput>
|
||||
|
||||
<OlDropdown label={activeRadioSettings ? activeRadioSettings.TACAN.XY : "X"} className={`
|
||||
my-auto w-20
|
||||
`}>
|
||||
<OlDropdown
|
||||
label={activeRadioSettings ? activeRadioSettings.TACAN.XY : "X"}
|
||||
className={`my-auto w-20`}
|
||||
>
|
||||
<OlDropdownItem
|
||||
key={"X"}
|
||||
onClick={() => {
|
||||
@ -2208,9 +2225,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
|
||||
`}
|
||||
@ -2228,10 +2247,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
)}
|
||||
|
||||
<div className="flex content-center gap-2">
|
||||
<OlLocation
|
||||
location={selectedUnits[0].getPosition()}
|
||||
className={`w-[280px] text-sm`}
|
||||
/>
|
||||
<OlLocation location={selectedUnits[0].getPosition()} className={`
|
||||
w-[280px] text-sm
|
||||
`} />
|
||||
<div className="my-auto text-gray-200">{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -26,8 +26,6 @@ import { GameMasterMenu } from "./panels/gamemastermenu";
|
||||
import { InfoBar } from "./panels/infobar";
|
||||
import { HotGroupBar } from "./panels/hotgroupsbar";
|
||||
import { SpawnContextMenu } from "./contextmenus/spawncontextmenu";
|
||||
import { CoordinatesPanel } from "./panels/coordinatespanel";
|
||||
import { RadiosSummaryPanel } from "./panels/radiossummarypanel";
|
||||
import { ServerOverlay } from "./serveroverlay";
|
||||
import { ImportExportModal } from "./modals/importexportmodal";
|
||||
import { WarningModal } from "./modals/warningmodal";
|
||||
|
||||
@ -7,7 +7,7 @@ var packetID = 0;
|
||||
export enum MessageType {
|
||||
audio,
|
||||
settings,
|
||||
unitIDs
|
||||
clientsData
|
||||
}
|
||||
|
||||
export class AudioPacket {
|
||||
|
||||
@ -58,22 +58,31 @@ export class SRSHandler {
|
||||
if (error) console.log(`Error pinging SRS server on UDP: ${error}`);
|
||||
});
|
||||
|
||||
if (this.tcp.readyState == "open")
|
||||
this.tcp.write(`${JSON.stringify(SYNC)}\n`);
|
||||
if (this.tcp.readyState == "open") this.tcp.write(`${JSON.stringify(SYNC)}\n`);
|
||||
else clearInterval(this.syncInterval);
|
||||
|
||||
let unitsBuffer = Buffer.from(
|
||||
JSON.stringify({
|
||||
unitIDs: this.clients.map((client) => {
|
||||
return client.RadioInfo.unitId;
|
||||
clientsData: this.clients.map((client) => {
|
||||
return {
|
||||
name: client.Name,
|
||||
unitID: client.RadioInfo.unitId,
|
||||
iff: client.RadioInfo.iff,
|
||||
coalition: client.Coalition,
|
||||
radios: client.RadioInfo.radios.map((radio) => {
|
||||
return {
|
||||
frequency: radio.freq,
|
||||
modulation: radio.modulation,
|
||||
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
}),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
this.ws.send(
|
||||
([] as number[]).concat([MessageType.unitIDs], [...unitsBuffer])
|
||||
);
|
||||
this.ws.send(([] as number[]).concat([MessageType.clientsData], [...unitsBuffer]));
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
@ -96,10 +105,7 @@ export class SRSHandler {
|
||||
});
|
||||
|
||||
this.udp.on("message", (message, remote) => {
|
||||
if (this.ws && message.length > 22)
|
||||
this.ws.send(
|
||||
([] as number[]).concat([MessageType.audio], [...message])
|
||||
);
|
||||
if (this.ws && message.length > 22) this.ws.send(([] as number[]).concat([MessageType.audio], [...message]));
|
||||
});
|
||||
}
|
||||
|
||||
@ -115,6 +121,14 @@ export class SRSHandler {
|
||||
let message = JSON.parse(data.slice(1));
|
||||
this.data.ClientGuid = message.guid;
|
||||
this.data.Coalition = message.coalition;
|
||||
|
||||
/* First reset all the radios to default values */
|
||||
this.data.RadioInfo.radios.forEach((radio) => {
|
||||
radio.freq = defaultSRSData.RadioInfo.radios[0].freq;
|
||||
radio.modulation = defaultSRSData.RadioInfo.radios[0].modulation;
|
||||
});
|
||||
|
||||
/* Then update the radios with the new settings */
|
||||
message.settings.forEach((setting, idx) => {
|
||||
this.data.RadioInfo.radios[idx].freq = setting.frequency;
|
||||
this.data.RadioInfo.radios[idx].modulation = setting.modulation;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user