diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 0450876f..589a3cc9 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -35,16 +35,13 @@ export class AudioManager { #socket: WebSocket | null = null; #guid: string = makeID(22); #SRSClientUnitIDs: number[] = []; + #syncInterval: number; constructor() { ConfigLoadedEvent.on((config: OlympusConfig) => { config.audio.WSPort ? this.setPort(config.audio.WSPort) : this.setEndpoint(config.audio.WSEndpoint); }); - setInterval(() => { - this.#syncRadioSettings(); - }, 1000); - let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"]; PTTKeys.forEach((key, idx) => { getApp() @@ -59,6 +56,10 @@ export class AudioManager { } start() { + this.#syncInterval = window.setInterval(() => { + this.#syncRadioSettings(); + }, 1000); + this.#running = true; this.#audioContext = new AudioContext({ sampleRate: 16000 }); this.#playbackPipeline = new PlaybackPipeline(); @@ -72,7 +73,9 @@ export class AudioManager { else if (this.#port) this.#socket = new WebSocket(`ws://${wsAddress}:${this.#port}`); else console.error("The audio backend was enabled but no port/endpoint was provided in the configuration"); - this.#socket = new WebSocket(`wss://refugees.dcsolympus.com/audio`); // TODO: remove, used for testing! + if (!this.#socket) return; + + //this.#socket = new WebSocket(`wss://refugees.dcsolympus.com/audio`); // TODO: remove, used for testing! /* Log the opening of the connection */ this.#socket.addEventListener("open", (event) => { @@ -98,7 +101,8 @@ export class AudioManager { /* Extract the frequency value and play it on the speakers if we are listening to it*/ audioPacket.getFrequencies().forEach((frequencyInfo) => { - if (sink.getFrequency() === frequencyInfo.frequency && sink.getModulation() === frequencyInfo.modulation) { + if (sink.getFrequency() === frequencyInfo.frequency && sink.getModulation() === frequencyInfo.modulation && sink.getTuned()) { + sink.setReceiving(true); this.#playbackPipeline.playBuffer(audioPacket.getAudioData().buffer); } }); @@ -133,6 +137,9 @@ export class AudioManager { this.#sinks.forEach((sink) => sink.disconnect()); this.#sources = []; this.#sinks = []; + this.#socket?.close(); + + window.clearInterval(this.#syncInterval); AudioSourcesChangedEvent.dispatch(this.#sources); AudioSinksChangedEvent.dispatch(this.#sinks); diff --git a/frontend/react/src/audio/radiosink.ts b/frontend/react/src/audio/radiosink.ts index e9335ff4..32be9798 100644 --- a/frontend/react/src/audio/radiosink.ts +++ b/frontend/react/src/audio/radiosink.ts @@ -16,6 +16,8 @@ export class RadioSink extends AudioSink { #ptt = false; #tuned = false; #volume = 0.5; + #receiving = false; + #clearReceivingTimeout: number; constructor() { super(); @@ -99,6 +101,20 @@ export class RadioSink extends AudioSink { return this.#volume; } + setReceiving(receiving) { + // Only do it if actually changed + if (receiving !== this.#receiving) AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); + if (receiving) { + window.clearTimeout(this.#clearReceivingTimeout); + this.#clearReceivingTimeout = window.setTimeout(() => this.setReceiving(false), 500); + } + this.#receiving = receiving; + } + + getReceiving() { + return this.#receiving; + } + handleEncodedData(encodedAudioChunk: EncodedAudioChunk) { let arrayBuffer = new ArrayBuffer(encodedAudioChunk.byteLength); encodedAudioChunk.copyTo(arrayBuffer); diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index 55cf6f5d..891eba29 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -5,11 +5,13 @@ import { CommandModeOptions, OlympusConfig, ServerStatus, SpawnRequestTable } fr import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle"; import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; import { Airbase } from "./mission/airbase"; +import { Bullseye } from "./mission/bullseye"; import { Shortcut } from "./shortcut/shortcut"; import { MapHiddenTypes, MapOptions } from "./types/types"; import { ContextAction } from "./unit/contextaction"; import { ContextActionSet } from "./unit/contextactionset"; import { Unit } from "./unit/unit"; +import { LatLng } from "leaflet"; export class BaseOlympusEvent { static on(callback: () => void) { @@ -34,7 +36,7 @@ export class BaseUnitEvent { static dispatch(unit: Unit) { document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); console.log(`Event ${this.name} dispatched`); - console.log(unit) + console.log(unit); } } @@ -62,9 +64,9 @@ export class ConfigLoadedEvent { } static dispatch(config: OlympusConfig) { - document.dispatchEvent(new CustomEvent(this.name, {detail: config})); + document.dispatchEvent(new CustomEvent(this.name, { detail: config })); console.log(`Event ${this.name} dispatched`); - console.log(config) + console.log(config); } } @@ -91,7 +93,7 @@ export class InfoPopupEvent { } static dispatch(messages: string[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {messages}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { messages } })); console.log(`Event ${this.name} dispatched`); } } @@ -104,20 +106,20 @@ export class HideMenuEvent { } static dispatch(hidden: boolean) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {hidden}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { hidden } })); console.log(`Event ${this.name} dispatched`); } } export class ShortcutsChangedEvent { - static on(callback: (shortcuts: {[key: string]: Shortcut}) => void) { + static on(callback: (shortcuts: { [key: string]: Shortcut }) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { callback(ev.detail.shortcuts); }); } - static dispatch(shortcuts: {[key: string]: Shortcut}) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcuts}})); + static dispatch(shortcuts: { [key: string]: Shortcut }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcuts } })); console.log(`Event ${this.name} dispatched`); } } @@ -130,7 +132,7 @@ export class ShortcutChangedEvent { } static dispatch(shortcut: Shortcut) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcut}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcut } })); console.log(`Event ${this.name} dispatched`); } } @@ -143,7 +145,7 @@ export class BindShortcutRequestEvent { } static dispatch(shortcut: Shortcut) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcut}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcut } })); console.log(`Event ${this.name} dispatched`); } } @@ -157,7 +159,7 @@ export class HiddenTypesChangedEvent { } static dispatch(hiddenTypes: MapHiddenTypes) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {hiddenTypes}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { hiddenTypes } })); console.log(`Event ${this.name} dispatched`); } } @@ -170,7 +172,7 @@ export class MapOptionsChangedEvent { } static dispatch(mapOptions: MapOptions) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {mapOptions}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { mapOptions } })); console.log(`Event ${this.name} dispatched`); } } @@ -183,7 +185,7 @@ export class MapSourceChangedEvent { } static dispatch(source: string) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {source}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { source } })); console.log(`Event ${this.name} dispatched`); } } @@ -235,7 +237,7 @@ export class ContextActionSetChangedEvent { } static dispatch(contextActionSet: ContextActionSet | null) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {contextActionSet}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { contextActionSet } })); console.log(`Event ${this.name} dispatched`); } } @@ -248,7 +250,7 @@ export class ContextActionChangedEvent { } static dispatch(contextAction: ContextAction | null) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {contextAction}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { contextAction } })); console.log(`Event ${this.name} dispatched`); } } @@ -258,11 +260,11 @@ export class UnitUpdatedEvent extends BaseUnitEvent { document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); // Logging disabled since periodic } -}; -export class UnitSelectedEvent extends BaseUnitEvent {}; -export class UnitDeselectedEvent extends BaseUnitEvent {}; -export class UnitDeadEvent extends BaseUnitEvent {}; -export class SelectionClearedEvent extends BaseOlympusEvent {}; +} +export class UnitSelectedEvent extends BaseUnitEvent {} +export class UnitDeselectedEvent extends BaseUnitEvent {} +export class UnitDeadEvent extends BaseUnitEvent {} +export class SelectionClearedEvent extends BaseOlympusEvent {} export class SelectedUnitsChangedEvent { static on(callback: (selectedUnits: Unit[]) => void) { @@ -272,9 +274,9 @@ export class SelectedUnitsChangedEvent { } static dispatch(selectedUnits: Unit[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: selectedUnits})); + document.dispatchEvent(new CustomEvent(this.name, { detail: selectedUnits })); console.log(`Event ${this.name} dispatched`); - console.log(selectedUnits) + console.log(selectedUnits); } } @@ -286,7 +288,7 @@ export class UnitExplosionRequestEvent { } static dispatch(units: Unit[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {units}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { units } })); console.log(`Event ${this.name} dispatched`); } } @@ -299,7 +301,7 @@ export class FormationCreationRequestEvent { } static dispatch(leader: Unit, wingmen: Unit[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {leader, wingmen}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { leader, wingmen } })); console.log(`Event ${this.name} dispatched`); } } @@ -312,7 +314,7 @@ export class MapContextMenuRequestEvent { } static dispatch(latlng: L.LatLng) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {latlng}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng } })); console.log(`Event ${this.name} dispatched`); } } @@ -325,7 +327,7 @@ export class UnitContextMenuRequestEvent { } static dispatch(unit: Unit) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {unit}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); console.log(`Event ${this.name} dispatched`); } } @@ -338,33 +340,46 @@ export class StarredSpawnContextMenuRequestEvent { } static dispatch(latlng: L.LatLng) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {latlng}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng } })); console.log(`Event ${this.name} dispatched`); } } export class HotgroupsChangedEvent { - static on(callback: (hotgroups: {[key: number]: number}) => void) { + static on(callback: (hotgroups: { [key: number]: number }) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { callback(ev.detail.hotgroups); }); } - static dispatch(hotgroups: {[key: number]: number}) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {hotgroups}})); + static dispatch(hotgroups: { [key: number]: number }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { hotgroups } })); console.log(`Event ${this.name} dispatched`); } } export class StarredSpawnsChangedEvent { - static on(callback: (starredSpawns: {[key: number]: SpawnRequestTable}) => void) { + static on(callback: (starredSpawns: { [key: number]: SpawnRequestTable }) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { callback(ev.detail.starredSpawns); }); } - static dispatch(starredSpawns: {[key: number]: SpawnRequestTable}) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {starredSpawns}})); + static dispatch(starredSpawns: { [key: number]: SpawnRequestTable }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { starredSpawns } })); + console.log(`Event ${this.name} dispatched`); + } +} + +export class MouseMovedEvent { + static on(callback: (latlng: LatLng, elevation: number) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.latlng, ev.detail.elevation); + }); + } + + static dispatch(latlng: LatLng, elevation?: number) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng, elevation } })); console.log(`Event ${this.name} dispatched`); } } @@ -378,7 +393,7 @@ export class CommandModeOptionsChangedEvent { } static dispatch(options: CommandModeOptions) { - document.dispatchEvent(new CustomEvent(this.name, {detail: options})); + document.dispatchEvent(new CustomEvent(this.name, { detail: options })); console.log(`Event ${this.name} dispatched`); } } @@ -392,9 +407,9 @@ export class AudioSourcesChangedEvent { } static dispatch(audioSources: AudioSource[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {audioSources}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { audioSources } })); console.log(`Event ${this.name} dispatched`); - console.log(audioSources) + console.log(audioSources); } } @@ -406,9 +421,9 @@ export class AudioSinksChangedEvent { } static dispatch(audioSinks: AudioSink[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {audioSinks}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { audioSinks } })); console.log(`Event ${this.name} dispatched`); - console.log(audioSinks) + console.log(audioSinks); } } @@ -433,7 +448,21 @@ export class AudioManagerStateChangedEvent { } static dispatch(state: boolean) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {state}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { state } })); + console.log(`Event ${this.name} dispatched`); + } +} + +/************** Mission data events ***************/ +export class BullseyesDataChanged { + static on(callback: (bullseyes: { [name: string]: Bullseye }) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.bullseyes); + }); + } + + static dispatch(bullseyes: { [name: string]: Bullseye } ) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { bullseyes } })); console.log(`Event ${this.name} dispatched`); } } diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 26ddad32..ce1abb35 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -51,6 +51,7 @@ import { MapContextMenuRequestEvent, MapOptionsChangedEvent, MapSourceChangedEvent, + MouseMovedEvent, SelectionClearedEvent, StarredSpawnContextMenuRequestEvent, StarredSpawnsChangedEvent, @@ -517,9 +518,7 @@ export class Map extends L.Map { } getCurrentControls() { - const touch = matchMedia("(hover: none)").matches; - return []; - // TODO, is this a good idea? I never look at the thing + //const touch = matchMedia("(hover: none)").matches; //if (getApp().getState() === IDLE) { // return [ // { @@ -544,7 +543,7 @@ export class Map extends L.Map { // text: "Move map location", // }, // ]; - //} else if (getApp().getState() === SPAWN_UNIT) { + //} else if (getApp().getState() === OlympusState.SPAWN_UNIT) { // return [ // { // actions: [touch ? faHandPointer : "LMB"], @@ -812,7 +811,7 @@ export class Map extends L.Map { } this.#miniMap = new ClickableMiniMap(this.#miniMapLayerGroup, { - position: "topright", + position: "bottomright", width: 192 * 1.5, height: 108 * 1.5, //@ts-ignore Needed because some of the inputs are wrong in the original module interface @@ -999,7 +998,7 @@ export class Map extends L.Map { return; } - if (this.#contextAction?.getTarget() === ContextActionTarget.POINT && e.originalEvent.button === 2) this.#isRotatingDestination = true; + if (this.#contextAction?.getTarget() === ContextActionTarget.POINT && e.originalEvent?.button === 2) this.#isRotatingDestination = true; this.scrollWheelZoom.disable(); this.#shortPressTimer = window.setTimeout(() => { @@ -1043,7 +1042,7 @@ export class Map extends L.Map { /* Do nothing */ } else if (getApp().getState() === OlympusState.SPAWN) { if (getApp().getSubState() === SpawnSubState.SPAWN_UNIT) { - if (e.originalEvent.button != 2 && this.#spawnRequestTable !== null) { + if (e.originalEvent?.button != 2 && this.#spawnRequestTable !== null) { this.#spawnRequestTable.unit.location = pressLocation; getApp() .getUnitsManager() @@ -1060,7 +1059,7 @@ export class Map extends L.Map { ); } } else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) { - if (e.originalEvent.button != 2 && this.#effectRequestTable !== null) { + if (e.originalEvent?.button != 2 && this.#effectRequestTable !== null) { if (this.#effectRequestTable.type === "explosion") { if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", pressLocation); else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", pressLocation); @@ -1099,10 +1098,10 @@ export class Map extends L.Map { } } } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (e.type === "touchstart" || e.originalEvent.buttons === 1) { + if (e.type === "touchstart" || e.originalEvent?.buttons === 1) { if (this.#contextAction !== null) this.executeContextAction(null, pressLocation, e.originalEvent); else getApp().setState(OlympusState.IDLE); - } else if (e.originalEvent.buttons === 2) { + } else if (e.originalEvent?.buttons === 2) { this.executeDefaultContextAction(null, pressLocation, e.originalEvent); } } else if (getApp().getState() === OlympusState.JTAC) { @@ -1172,7 +1171,7 @@ export class Map extends L.Map { if (e.type === "touchstart") document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e })); else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent })); } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (e.originalEvent.button === 2) { + if (e.originalEvent?.button === 2) { if (!this.getContextAction()) { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU); MapContextMenuRequestEvent.dispatch(pressLocation); @@ -1198,6 +1197,11 @@ export class Map extends L.Map { this.#lastMousePosition.y = e.originalEvent.y; this.#lastMouseCoordinates = e.latlng; + MouseMovedEvent.dispatch(e.latlng); + getGroundElevation(e.latlng, (elevation) => { + MouseMovedEvent.dispatch(e.latlng, elevation); + }) + if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng); if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng); } else { @@ -1269,17 +1273,6 @@ export class Map extends L.Map { #setSlaveDCSCameraAvailable(newSlaveDCSCameraAvailable: boolean) { this.#slaveDCSCameraAvailable = newSlaveDCSCameraAvailable; - let linkButton = document.getElementById("camera-link-control"); - if (linkButton) { - if (!newSlaveDCSCameraAvailable) { - //this.setSlaveDCSCamera(false); // Commented to experiment with usability - linkButton.classList.add("red"); - linkButton.title = "Camera link to DCS is not available"; - } else { - linkButton.classList.remove("red"); - linkButton.title = "Link/Unlink DCS camera with Olympus position"; - } - } } /* Check if the camera control plugin is available. Right now this will only change the color of the button, no changes in functionality */ diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index 977ccd3a..4d565abe 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -7,7 +7,7 @@ import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionDa import { Coalition } from "../types/types"; import { Carrier } from "./carrier"; import { NavyUnit } from "../unit/unit"; -import { CommandModeOptionsChangedEvent, InfoPopupEvent } from "../events"; +import { BullseyesDataChanged, CommandModeOptionsChangedEvent, InfoPopupEvent } from "../events"; /** The MissionManager */ export class MissionManager { @@ -56,6 +56,8 @@ export class MissionManager { this.#bullseyes[idx].setLatLng(new LatLng(bullseye.latitude, bullseye.longitude)); this.#bullseyes[idx].setCoalition(bullseye.coalition); } + + BullseyesDataChanged.dispatch(this.#bullseyes) } } diff --git a/frontend/react/src/ui/panels/components/radiosinkpanel.tsx b/frontend/react/src/ui/panels/components/radiosinkpanel.tsx index 7a332c9e..d9d8788b 100644 --- a/frontend/react/src/ui/panels/components/radiosinkpanel.tsx +++ b/frontend/react/src/ui/panels/components/radiosinkpanel.tsx @@ -12,13 +12,15 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey useEffect(() => { if (props.onExpanded) props.onExpanded(); - }, [expanded]) + }, [expanded]); return (