DCSOlympus/frontend/react/src/server/servermanager.ts
2025-01-29 11:13:55 +01:00

738 lines
24 KiB
TypeScript

import { LatLng } from "leaflet";
import { getApp } from "../olympusapp";
import {
AIRBASES_URI,
BULLSEYE_URI,
COMMANDS_URI,
LOGS_URI,
MISSION_URI,
NONE,
ROEs,
UNITS_URI,
WEAPONS_URI,
emissionsCountermeasures,
reactionsToThreat,
} from "../constants/constants";
import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from "../interfaces";
import { MapOptionsChangedEvent, ServerStatusUpdatedEvent, WrongCredentialsEvent } from "../events";
export class ServerManager {
#connected: boolean = false;
#paused: boolean = false;
#REST_ADDRESS = "./olympus";
#username: null | string = null;
#password: null | string = null;
#sessionHash: string | null = null;
#lastUpdateTimes: { [key: string]: number } = {};
#previousMissionElapsedTime: number = 0; // Track if mission elapsed time is increasing (i.e. is the server paused)
#serverIsPaused: boolean = false;
#intervals: number[] = [];
#requests: { [key: string]: XMLHttpRequest } = {};
#updateMode = "normal"; // normal or awacs
#activeCommandMode = "";
constructor() {
this.#lastUpdateTimes[UNITS_URI] = Date.now();
this.#lastUpdateTimes[WEAPONS_URI] = Date.now();
this.#lastUpdateTimes[LOGS_URI] = Date.now();
this.#lastUpdateTimes[AIRBASES_URI] = Date.now();
this.#lastUpdateTimes[BULLSEYE_URI] = Date.now();
this.#lastUpdateTimes[MISSION_URI] = Date.now();
getApp().getShortcutManager().addShortcut("togglePause", {
label: "Pause data update",
keyUpCallback: () => {
this.setPaused(!this.getPaused());
},
code: "Enter"
})
MapOptionsChangedEvent.on((mapOptions) => {
/* TODO if (this.#updateMode === "normal" && mapOptions.AWACSMode) {
this.#updateMode = "awacs";
this.startUpdate();
} else if (this.#updateMode === "awacs" && !mapOptions.AWACSMode) {
this.#updateMode = "normal";
this.startUpdate();
} */
})
}
setUsername(newUsername: string) {
this.#username = newUsername;
}
getUsername() {
return this.#username;
}
setPassword(newPassword: string) {
this.#password = newPassword;
}
setActiveCommandMode(activeCommandMode: string) {
this.#activeCommandMode = activeCommandMode;
}
GET(
callback: CallableFunction,
errorCallback: CallableFunction,
uri: string,
options?: ServerRequestOptions,
responseType: string = "text",
force: boolean = false
) {
var xmlHttp = new XMLHttpRequest();
/* If a request on this uri is still pending (meaning it's not done or did not yet fail), skip the request, to avoid clogging the TCP workers */
/* If we are forcing the request we don't care if one already exists, just send it. CAREFUL: this makes sense only for low frequency requests, like refreshes, when we
are reasonably confident any previous request will be done before we make a new one on the same URI. */
if (uri in this.#requests && this.#requests[uri].readyState !== 4 && !force) {
//console.warn(`GET request on ${uri} URI still pending, skipping...`);
return;
}
if (!force) this.#requests[uri] = xmlHttp;
/* Assemble the request options string */
var optionsString = "";
if (options?.time != undefined) optionsString = `time=${options.time}`;
if (options?.commandHash != undefined) optionsString = `commandHash=${options.commandHash}`;
/* On the connection */
xmlHttp.open("GET", `${this.#REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ""}`, true);
/* If provided, set the credentials */
xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username ?? ""}:${this.#password ?? ""}`));
xmlHttp.setRequestHeader("X-Command-Mode", this.#activeCommandMode);
/* If specified, set the response type */
if (responseType) xmlHttp.responseType = responseType as XMLHttpRequestResponseType;
xmlHttp.onload = (e) => {
if (xmlHttp.status == 200) {
/* Success */
this.setConnected(true);
if (xmlHttp.responseType == "arraybuffer") this.#lastUpdateTimes[uri] = callback(xmlHttp.response);
else {
/* Check if the response headers contain the enabled command modes and set them */
if (xmlHttp.getResponseHeader("X-Enabled-Command-Modes"))
getApp().getMissionManager().setEnabledCommandModes(xmlHttp.getResponseHeader("X-Enabled-Command-Modes")?.split(",") ??[])
const result = JSON.parse(xmlHttp.responseText);
this.#lastUpdateTimes[uri] = callback(result);
if (result.frameRate !== undefined && result.load !== undefined) {
getApp().getMissionManager().setLoad(result.load);
getApp().getMissionManager().setFrameRate(result.frameRate);
}
}
} else if (xmlHttp.status == 401) {
/* Bad credentials */
console.error("Incorrect username/password");
WrongCredentialsEvent.dispatch();
errorCallback && errorCallback(xmlHttp.status);
} else {
/* Failure, probably disconnected */
this.setConnected(false);
errorCallback && errorCallback(xmlHttp.status);
}
};
xmlHttp.onreadystatechange = (res) => {
if (xmlHttp.readyState == 4 && xmlHttp.status === 0) {
console.error("An error occurred during the XMLHttpRequest");
this.setConnected(false);
errorCallback && errorCallback(xmlHttp.status);
}
};
xmlHttp.send(null);
}
PUT(request: object, callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", this.#REST_ADDRESS);
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username ?? ""}:${this.#password ?? ""}`));
xmlHttp.setRequestHeader("X-Command-Mode", this.#activeCommandMode);
xmlHttp.onload = (res: any) => {
var res = JSON.parse(xmlHttp.responseText);
callback(res.commandHash);
};
xmlHttp.send(JSON.stringify(request));
console.log(`Sending PUT request:`);
console.log(request);
}
getConfig(callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", "./config", true);
xmlHttp.onload = function (e) {
var data = JSON.parse(xmlHttp.responseText);
callback(data);
};
xmlHttp.onerror = function () {
console.error("An error occurred during the XMLHttpRequest, could not retrieve configuration file");
};
xmlHttp.send(null);
}
getAirbases(callback: CallableFunction, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, AIRBASES_URI);
}
getBullseye(callback: CallableFunction, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, BULLSEYE_URI);
}
getLogs(callback: CallableFunction, refresh: boolean = false, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, LOGS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[LOGS_URI] }, "text", refresh);
}
getMission(callback: CallableFunction, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, MISSION_URI);
}
getUnits(callback: CallableFunction, refresh: boolean = false, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, UNITS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[UNITS_URI] }, "arraybuffer", refresh);
}
getWeapons(callback: CallableFunction, refresh: boolean = false, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, WEAPONS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[WEAPONS_URI] }, "arraybuffer", refresh);
}
isCommandExecuted(callback: CallableFunction, commandHash: string, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, COMMANDS_URI, {
commandHash: commandHash,
});
}
addDestination(ID: number, path: any, callback: CallableFunction = () => {}) {
var command = { ID: ID, path: path };
var data = { setPath: command };
this.PUT(data, callback);
}
spawnSmoke(color: string, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { color: color, location: latlng };
var data = { smoke: command };
this.PUT(data, callback);
}
spawnExplosion(intensity: number, explosionType: string, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = {
explosionType: explosionType,
intensity: intensity,
location: latlng,
};
var data = { explosion: command };
this.PUT(data, callback);
}
spawnAircrafts(
units: any,
coalition: string,
airbaseName: string,
country: string,
immediate: boolean,
spawnPoints: number,
callback: CallableFunction = () => {}
) {
var command = {
units: units,
coalition: coalition,
airbaseName: airbaseName,
country: country,
immediate: immediate,
spawnPoints: spawnPoints,
};
var data = { spawnAircrafts: command };
this.PUT(data, callback);
}
spawnHelicopters(
units: any,
coalition: string,
airbaseName: string,
country: string,
immediate: boolean,
spawnPoints: number,
callback: CallableFunction = () => {}
) {
var command = {
units: units,
coalition: coalition,
airbaseName: airbaseName,
country: country,
immediate: immediate,
spawnPoints: spawnPoints,
};
var data = { spawnHelicopters: command };
this.PUT(data, callback);
}
spawnGroundUnits(units: any, coalition: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => {}) {
var command = {
units: units,
coalition: coalition,
country: country,
immediate: immediate,
spawnPoints: spawnPoints,
};
var data = { spawnGroundUnits: command };
this.PUT(data, callback);
}
spawnNavyUnits(units: any, coalition: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => {}) {
var command = {
units: units,
coalition: coalition,
country: country,
immediate: immediate,
spawnPoints: spawnPoints,
};
var data = { spawnNavyUnits: command };
this.PUT(data, callback);
}
attackUnit(ID: number, targetID: number, callback: CallableFunction = () => {}) {
var command = { ID: ID, targetID: targetID };
var data = { attackUnit: command };
this.PUT(data, callback);
}
followUnit(ID: number, targetID: number, offset: { x: number; y: number; z: number }, callback: CallableFunction = () => {}) {
// X: front-rear, positive front
// Y: top-bottom, positive bottom
// Z: left-right, positive right
var command = {
ID: ID,
targetID: targetID,
offsetX: offset["x"],
offsetY: offset["y"],
offsetZ: offset["z"],
};
var data = { followUnit: command };
this.PUT(data, callback);
}
cloneUnits(units: { ID: number; location: LatLng }[], deleteOriginal: boolean, spawnPoints: number, callback: CallableFunction = () => {}) {
var command = {
units: units,
deleteOriginal: deleteOriginal,
spawnPoints: spawnPoints,
};
var data = { cloneUnits: command };
this.PUT(data, callback);
}
deleteUnit(ID: number, explosion: boolean, explosionType: string, immediate: boolean, callback: CallableFunction = () => {}) {
var command = {
ID: ID,
explosion: explosion,
explosionType: explosionType,
immediate: immediate,
};
var data = { deleteUnit: command };
this.PUT(data, callback);
}
landAt(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { ID: ID, location: latlng };
var data = { landAt: command };
this.PUT(data, callback);
}
changeSpeed(ID: number, speedChange: string, callback: CallableFunction = () => {}) {
var command = { ID: ID, change: speedChange };
var data = { changeSpeed: command };
this.PUT(data, callback);
}
setSpeed(ID: number, speed: number, callback: CallableFunction = () => {}) {
var command = { ID: ID, speed: speed };
var data = { setSpeed: command };
this.PUT(data, callback);
}
setSpeedType(ID: number, speedType: string, callback: CallableFunction = () => {}) {
var command = { ID: ID, speedType: speedType };
var data = { setSpeedType: command };
this.PUT(data, callback);
}
changeAltitude(ID: number, altitudeChange: string, callback: CallableFunction = () => {}) {
var command = { ID: ID, change: altitudeChange };
var data = { changeAltitude: command };
this.PUT(data, callback);
}
setAltitudeType(ID: number, altitudeType: string, callback: CallableFunction = () => {}) {
var command = { ID: ID, altitudeType: altitudeType };
var data = { setAltitudeType: command };
this.PUT(data, callback);
}
setAltitude(ID: number, altitude: number, callback: CallableFunction = () => {}) {
var command = { ID: ID, altitude: altitude };
var data = { setAltitude: command };
this.PUT(data, callback);
}
setROE(ID: number, ROE: string, callback: CallableFunction = () => {}) {
var command = { ID: ID, ROE: ROEs.indexOf(ROE) };
var data = { setROE: command };
this.PUT(data, callback);
}
setReactionToThreat(ID: number, reactionToThreat: string, callback: CallableFunction = () => {}) {
var command = {
ID: ID,
reactionToThreat: reactionsToThreat.indexOf(reactionToThreat),
};
var data = { setReactionToThreat: command };
this.PUT(data, callback);
}
setEmissionsCountermeasures(ID: number, emissionCountermeasure: string, callback: CallableFunction = () => {}) {
var command = {
ID: ID,
emissionsCountermeasures: emissionsCountermeasures.indexOf(emissionCountermeasure),
};
var data = { setEmissionsCountermeasures: command };
this.PUT(data, callback);
}
setOnOff(ID: number, onOff: boolean, callback: CallableFunction = () => {}) {
var command = { ID: ID, onOff: onOff };
var data = { setOnOff: command };
this.PUT(data, callback);
}
setFollowRoads(ID: number, followRoads: boolean, callback: CallableFunction = () => {}) {
var command = { ID: ID, followRoads: followRoads };
var data = { setFollowRoads: command };
this.PUT(data, callback);
}
setOperateAs(ID: number, operateAs: number, callback: CallableFunction = () => {}) {
var command = { ID: ID, operateAs: operateAs };
var data = { setOperateAs: command };
this.PUT(data, callback);
}
refuel(ID: number, callback: CallableFunction = () => {}) {
var command = { ID: ID };
var data = { refuel: command };
this.PUT(data, callback);
}
bombPoint(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { ID: ID, location: latlng };
var data = { bombPoint: command };
this.PUT(data, callback);
}
carpetBomb(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { ID: ID, location: latlng };
var data = { carpetBomb: command };
this.PUT(data, callback);
}
bombBuilding(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { ID: ID, location: latlng };
var data = { bombBuilding: command };
this.PUT(data, callback);
}
fireAtArea(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { ID: ID, location: latlng };
var data = { fireAtArea: command };
this.PUT(data, callback);
}
fireLaser(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { ID: ID, location: latlng, code: 1688 };
var data = { fireLaser: command };
this.PUT(data, callback);
}
fireInfrared(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { ID: ID, location: latlng };
var data = { fireInfrared: command };
this.PUT(data, callback);
}
simulateFireFight(ID: number, latlng: LatLng, altitude: number, callback: CallableFunction = () => {}) {
var command = { ID: ID, location: latlng, altitude: altitude };
var data = { simulateFireFight: command };
this.PUT(data, callback);
}
// TODO: Remove coalition
scenicAAA(ID: number, coalition: string, callback: CallableFunction = () => {}) {
var command = { ID: ID, coalition: coalition };
var data = { scenicAAA: command };
this.PUT(data, callback);
}
// TODO: Remove coalition
missOnPurpose(ID: number, coalition: string, callback: CallableFunction = () => {}) {
var command = { ID: ID, coalition: coalition };
var data = { missOnPurpose: command };
this.PUT(data, callback);
}
landAtPoint(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { ID: ID, location: latlng };
var data = { landAtPoint: command };
this.PUT(data, callback);
}
setShotsScatter(ID: number, shotsScatter: number, callback: CallableFunction = () => {}) {
var command = { ID: ID, shotsScatter: shotsScatter };
var data = { setShotsScatter: command };
this.PUT(data, callback);
}
setShotsIntensity(ID: number, shotsIntensity: number, callback: CallableFunction = () => {}) {
var command = { ID: ID, shotsIntensity: shotsIntensity };
var data = { setShotsIntensity: command };
this.PUT(data, callback);
}
setRacetrack(ID: number, length: number, latlng: LatLng, bearing: number, callback: CallableFunction = () => {}) {
var command = { ID: ID, location: latlng, bearing: bearing, length: length };
var data = { setRacetrack: command };
this.PUT(data, callback);
}
setAdvancedOptions(
ID: number,
isActiveTanker: boolean,
isActiveAWACS: boolean,
TACAN: TACAN,
radio: Radio,
generalSettings: GeneralSettings,
callback: CallableFunction = () => {}
) {
var command = {
ID: ID,
isActiveTanker: isActiveTanker,
isActiveAWACS: isActiveAWACS,
TACAN: TACAN,
radio: radio,
generalSettings: generalSettings,
};
var data = { setAdvancedOptions: command };
this.PUT(data, callback);
}
setCommandModeOptions(
commandModeOptions: CommandModeOptions,
callback: CallableFunction = () => {}
) {
var data = { setCommandModeOptions: commandModeOptions };
this.PUT(data, callback);
}
reloadDatabases(callback: CallableFunction = () => {}) {
var data = { reloadDatabases: {} };
this.PUT(data, callback);
}
startUpdate() {
/* Clear any existing interval */
this.#intervals.forEach((interval: number) => {
window.clearInterval(interval);
});
this.#intervals = [];
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused()) {
this.getMission((data: MissionData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateMission(data);
return data.time;
});
}
}, 1000)
);
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getAirbases((data: AirbasesData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateAirbases(data);
return data.time;
});
}
}, 10000)
);
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getBullseye((data: BullseyesData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateBullseyes(data);
return data.time;
});
}
}, 10000)
);
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getLogs((data: any) => {
this.checkSessionHash(data.sessionHash);
//(getApp().getPanelsManager().get("log") as LogPanel).appendLogs(data.logs)
return data.time;
});
}
}, 1000)
);
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer, false);
return time;
}, false);
}
}, this.#updateMode === "normal"? 250: 2000)
);
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getWeapons((buffer: ArrayBuffer) => {
var time = getApp().getWeaponsManager()?.update(buffer, false);
return time;
}, false);
}
}, this.#updateMode === "normal"? 250: 2000)
);
this.#intervals.push(
window.setInterval(
() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer, true);
return time;
}, true);
}
},
5000
)
);
// Mission clock and elapsed time
this.#intervals.push(
window.setInterval(() => {
const elapsedMissionTime = getApp().getMissionManager().getDateAndTime().elapsedTime;
this.#serverIsPaused = elapsedMissionTime === this.#previousMissionElapsedTime;
this.#previousMissionElapsedTime = elapsedMissionTime;
ServerStatusUpdatedEvent.dispatch({
frameRate: getApp().getMissionManager().getFrameRate(),
load: getApp().getMissionManager().getLoad(),
elapsedTime: getApp().getMissionManager().getDateAndTime().elapsedTime,
missionTime: getApp().getMissionManager().getDateAndTime().time,
connected: this.getConnected(),
paused: this.getPaused(),
} as ServerStatus);
}, 1000)
);
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getWeapons((buffer: ArrayBuffer) => {
var time = getApp().getWeaponsManager()?.update(buffer, true);
return time;
}, true);
}
}, 5000)
);
}
refreshAll() {
this.getAirbases((data: AirbasesData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateAirbases(data);
return data.time;
});
this.getBullseye((data: BullseyesData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateBullseyes(data);
return data.time;
});
this.getLogs((data: any) => {
this.checkSessionHash(data.sessionHash);
//(getApp().getPanelsManager().get("log") as LogPanel).appendLogs(data.logs)
return data.time;
});
this.getWeapons((buffer: ArrayBuffer) => {
var time = getApp().getWeaponsManager()?.update(buffer, true);
return time;
}, true);
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer, true);
return time;
}, true);
}
checkSessionHash(newSessionHash: string) {
if (this.#sessionHash != null) {
if (newSessionHash !== this.#sessionHash) location.reload();
} else {
this.#sessionHash = newSessionHash;
getApp().getSessionDataManager().loadSessionData(newSessionHash);
}
}
setConnected(newConnected: boolean) {
if (this.#connected != newConnected) {
newConnected ? getApp().addInfoMessage("Connected to DCS Olympus server") : getApp().addInfoMessage("Disconnected from DCS Olympus server");
if (newConnected) {
document.getElementById("splash-screen")?.classList.add("hide");
document.getElementById("gray-out")?.classList.add("hide");
}
}
this.#connected = newConnected;
}
getConnected() {
return this.#connected;
}
setPaused(newPaused: boolean) {
this.#paused = newPaused;
this.#paused ? getApp().addInfoMessage("View paused") : getApp().addInfoMessage("View unpaused");
}
getPaused() {
return this.#paused;
}
getServerIsPaused() {
return this.#serverIsPaused;
}
getRequests() {
return this.#requests;
}
}