diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 4fd77a29..7da5ae1c 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -300,6 +300,13 @@ export enum UnitControlSubState { UNIT_EXPLOSION_MENU = "Unit explosion menu", } +export enum LoginSubState { + NO_SUBSTATE = "No substate", + CREDENTIALS = "Credentials", + COMMAND_MODE = "Command mode", + CONNECT = "Connect" +} + export enum DrawSubState { NO_SUBSTATE = "No substate", DRAW_POLYGON = "Polygon", diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 32d9edd1..c34e3e22 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -4,6 +4,11 @@ import { MapOptions } from "./types/types"; export interface OlympusConfig { frontend: { port: number; + customAuthHeaders: { + enabled: boolean, + username: string, + group: string + } elevationProvider: { provider: string; username: string | null; @@ -26,7 +31,7 @@ export interface OlympusConfig { WSPort?: number; WSEndpoint?: string; }; - profiles?: ProfileOptions; + profiles?: {[key: string]: ProfileOptions}; } export interface SessionData { @@ -277,6 +282,7 @@ export interface UnitBlueprint { indirectFire?: boolean; markerFile?: string; unitWhenGrouped?: string; + mainRole?: string; } export interface AirbaseOptions { diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 80f58d9c..a582a05b 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -546,8 +546,9 @@ export class Map extends L.Map { this.#spawnRequestTable = spawnRequestTable; } - addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable) { + addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable, quickAccessName: string) { this.#starredSpawnRequestTables[key] = spawnRequestTable; + this.#starredSpawnRequestTables[key].quickAccessName = quickAccessName; StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables); } diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index f95ff5de..79f107e6 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -32,6 +32,7 @@ export class MissionManager { #remainingSetupTime: number = 0; #spentSpawnPoint: number = 0; #coalitions: { red: string[]; blue: string[] } = { red: [], blue: [] }; + #enabledCommandModes: string[] = []; constructor() { AppStateChangedEvent.on((state, subState) => { @@ -220,6 +221,14 @@ export class MissionManager { return airbase ?? null; } + setEnabledCommandModes(enabledCommandModes: string[]) { + this.#enabledCommandModes = enabledCommandModes; + } + + getEnabledCommandModes() { + return this.#enabledCommandModes; + } + #setcommandModeOptions(commandModeOptions: CommandModeOptions) { /* Refresh all the data if we have exited the NONE state */ var requestRefresh = false; diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index d7cc97c7..0e2fd653 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -20,7 +20,7 @@ import { WeaponsManager } from "./weapon/weaponsmanager"; import { ServerManager } from "./server/servermanager"; import { AudioManager } from "./audio/audiomanager"; -import { NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants"; +import { LoginSubState, NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants"; import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events"; import { OlympusConfig, ProfileOptions } from "./interfaces"; import { AWACSController } from "./controllers/awacs"; @@ -32,27 +32,26 @@ export var IP = window.location.toString(); export class OlympusApp { /* Global data */ #latestVersion: string | undefined = undefined; - #config: OlympusConfig | null = null; + #config: OlympusConfig; #state: OlympusState = OlympusState.NOT_INITIALIZED; #subState: OlympusSubState = NO_SUBSTATE; #infoMessages: string[] = []; - #profileName: string | null = null; /* Main leaflet map, extended by custom methods */ - #map: Map | null = null; + #map: Map; /* Managers */ - #missionManager: MissionManager | null = null; - #serverManager: ServerManager | null = null; - #shortcutManager: ShortcutManager | null = null; - #unitsManager: UnitsManager | null = null; - #weaponsManager: WeaponsManager | null = null; - #audioManager: AudioManager | null = null; - #sessionDataManager: SessionDataManager | null = null; + #missionManager: MissionManager; + #serverManager: ServerManager; + #shortcutManager: ShortcutManager; + #unitsManager: UnitsManager; + #weaponsManager: WeaponsManager; + #audioManager: AudioManager; + #sessionDataManager: SessionDataManager; //#pluginsManager: // TODO /* Controllers */ - #AWACSController: AWACSController | null = null; + #AWACSController: AWACSController; constructor() { SelectedUnitsChangedEvent.on((selectedUnits) => { @@ -69,31 +68,31 @@ export class OlympusApp { } getServerManager() { - return this.#serverManager as ServerManager; + return this.#serverManager; } getShortcutManager() { - return this.#shortcutManager as ShortcutManager; + return this.#shortcutManager; } getUnitsManager() { - return this.#unitsManager as UnitsManager; + return this.#unitsManager; } getWeaponsManager() { - return this.#weaponsManager as WeaponsManager; + return this.#weaponsManager; } getMissionManager() { - return this.#missionManager as MissionManager; + return this.#missionManager; } getAudioManager() { - return this.#audioManager as AudioManager; + return this.#audioManager; } getSessionDataManager() { - return this.#sessionDataManager as SessionDataManager; + return this.#sessionDataManager; } /* TODO @@ -154,20 +153,37 @@ export class OlympusApp { }); /* Load the config file from the server */ - const configRequest = new Request(this.getExpressAddress() + "/resources/config"); + const configRequest = new Request(this.getExpressAddress() + "/resources/config", { + headers: { + 'Cache-Control': 'no-cache', + } + }); + fetch(configRequest) .then((response) => { - if (response.status === 200) { - return response.json(); - } else { - throw new Error("Error retrieving config file"); - } + if (response.status === 200) + return new Promise((res: ([OlympusConfig, Headers]) => void, rej) => { + response + .json() + .then((result) => res([result, response.headers])) + .catch((error) => rej(error)); + }); + else throw new Error("Error retrieving config file"); }) - .then((res) => { - this.#config = res; + .then(([result, headers]) => { + this.#config = result; + if (this.#config.frontend.customAuthHeaders.enabled) { + if (headers.has(this.#config.frontend.customAuthHeaders.username) && headers.has(this.#config.frontend.customAuthHeaders.group)) { + this.getServerManager().setUsername(headers.get(this.#config.frontend.customAuthHeaders.username)); + this.setState(OlympusState.LOGIN, LoginSubState.COMMAND_MODE); + } + } + if (this.getState() !== OlympusState.LOGIN) { + this.setState(OlympusState.LOGIN, LoginSubState.CREDENTIALS); + } ConfigLoadedEvent.dispatch(this.#config as OlympusConfig); - this.setState(OlympusState.LOGIN); - }); + }) + .catch((error) => console.error); this.#shortcutManager?.addShortcut("idle", { label: "Deselect all", @@ -184,12 +200,9 @@ export class OlympusApp { return this.#config; } - setProfile(profileName: string) { - this.#profileName = profileName; - } - saveProfile() { - if (this.#profileName !== null) { + const username = this.getServerManager()?.getUsername(); + if (username) { let profile = {}; profile["mapOptions"] = this.#map?.getOptions(); profile["shortcuts"] = this.#shortcutManager?.getShortcutsOptions(); @@ -200,10 +213,10 @@ export class OlympusApp { body: JSON.stringify(profile), // Send the data in JSON format }; - fetch(this.getExpressAddress() + `/resources/profile/${this.#profileName}`, requestOptions) + fetch(this.getExpressAddress() + `/resources/profile/${username}`, requestOptions) .then((response) => { if (response.status === 200) { - console.log(`Profile ${this.#profileName} saved correctly`); + console.log(`Profile for ${username} saved correctly`); } else { this.addInfoMessage("Error saving profile"); throw new Error("Error saving profile"); @@ -214,17 +227,18 @@ export class OlympusApp { } resetProfile() { - if (this.#profileName !== null) { + const username = this.getServerManager().getUsername(); + if (username) { const requestOptions = { method: "PUT", // Specify the request method headers: { "Content-Type": "application/json" }, // Specify the content type body: "", // Send the data in JSON format }; - fetch(this.getExpressAddress() + `/resources/profile/reset/${this.#profileName}`, requestOptions) + fetch(this.getExpressAddress() + `/resources/profile/reset/${username}`, requestOptions) .then((response) => { if (response.status === 200) { - console.log(`Profile ${this.#profileName} reset correctly`); + console.log(`Profile for ${username} reset correctly`); location.reload(); } else { this.addInfoMessage("Error resetting profile"); @@ -256,22 +270,19 @@ export class OlympusApp { } getProfile() { - if (this.#profileName && this.#config?.profiles && this.#config?.profiles[this.#profileName]) - return this.#config?.profiles[this.#profileName] as ProfileOptions; + const username = this.getServerManager().getUsername(); + if (username && this.#config?.profiles && username in this.#config.profiles) return this.#config?.profiles[username]; else return null; } - getProfileName() { - return this.#profileName; - } - loadProfile() { + const username = this.getServerManager().getUsername(); const profile = this.getProfile(); - if (profile) { + if (username && profile) { this.#map?.setOptions(profile.mapOptions); this.#shortcutManager?.setShortcutsOptions(profile.shortcuts); this.addInfoMessage("Profile loaded correctly"); - console.log(`Profile ${this.#profileName} loaded correctly`); + console.log(`Profile for ${username} loaded correctly`); } else { this.addInfoMessage("Profile not found, creating new profile"); console.log(`Error loading profile`); @@ -302,6 +313,4 @@ export class OlympusApp { InfoPopupEvent.dispatch(this.#infoMessages); }, 5000); } - - } diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index b9ca3b9b..e398937b 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -404,4 +404,26 @@ export function blobToBase64(blob) { reader.onloadend = () => resolve(reader.result as string); reader.readAsDataURL(blob); }); +} + +export function mode(array) +{ + if(array.length == 0) + return null; + var modeMap = {}; + var maxEl = array[0], maxCount = 1; + for(var i = 0; i < array.length; i++) + { + var el = array[i]; + if(modeMap[el] == null) + modeMap[el] = 1; + else + modeMap[el]++; + if(modeMap[el] > maxCount) + { + maxEl = el; + maxCount = modeMap[el]; + } + } + return maxEl; } \ No newline at end of file diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index 8e2f00e6..089b6911 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -20,8 +20,8 @@ export class ServerManager { #connected: boolean = false; #paused: boolean = false; #REST_ADDRESS = "http://localhost:3001/olympus"; - #username = "no-username"; - #password = ""; + #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) @@ -29,6 +29,7 @@ export class ServerManager { #intervals: number[] = []; #requests: { [key: string]: XMLHttpRequest } = {}; #updateMode = "normal"; // normal or awacs + #activeCommandMode = ""; constructor() { this.#lastUpdateTimes[UNITS_URI] = Date.now(); @@ -61,10 +62,18 @@ export class ServerManager { this.#username = newUsername; } + getUsername() { + return this.#username; + } + setPassword(newPassword: string) { this.#password = newPassword; } + setActiveCommandMode(activeCommandMode: string) { + this.#activeCommandMode = activeCommandMode; + } + GET( callback: CallableFunction, errorCallback: CallableFunction, @@ -94,7 +103,8 @@ export class ServerManager { xmlHttp.open("GET", `${this.#REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ""}`, true); /* If provided, set the credentials */ - if (this.#username && this.#password) xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username}:${this.#password}`)); + 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; @@ -105,6 +115,10 @@ export class ServerManager { 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); @@ -137,7 +151,8 @@ export class ServerManager { var xmlHttp = new XMLHttpRequest(); xmlHttp.open("PUT", this.#REST_ADDRESS); xmlHttp.setRequestHeader("Content-Type", "application/json"); - if (this.#username && this.#password) xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username}:${this.#password}`)); + 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); diff --git a/frontend/react/src/sessiondata.ts b/frontend/react/src/sessiondata.ts index c01b3d15..aabb4828 100644 --- a/frontend/react/src/sessiondata.ts +++ b/frontend/react/src/sessiondata.ts @@ -87,10 +87,10 @@ export class SessionDataManager { body: JSON.stringify({ sessionHash }), // Send the data in JSON format }; - fetch(getApp().getExpressAddress() + `/resources/sessiondata/load/${getApp().getProfileName()}`, requestOptions) + fetch(getApp().getExpressAddress() + `/resources/sessiondata/load/${getApp().getServerManager().getUsername()}`, requestOptions) .then((response) => { if (response.status === 200) { - console.log(`Session data for profile ${getApp().getProfileName()} and session hash ${sessionHash} loaded correctly`); + console.log(`Session data for profile ${getApp().getServerManager().getUsername()} and session hash ${sessionHash} loaded correctly`); return response.json(); } else { getApp().addInfoMessage("No session data found for this profile"); @@ -118,10 +118,10 @@ export class SessionDataManager { body: JSON.stringify({ sessionHash: this.#sessionHash, sessionData: this.#sessionData }), // Send the data in JSON format }; - fetch(getApp().getExpressAddress() + `/resources/sessiondata/save/${getApp().getProfileName()}`, requestOptions) + fetch(getApp().getExpressAddress() + `/resources/sessiondata/save/${getApp().getServerManager().getUsername()}`, requestOptions) .then((response) => { if (response.status === 200) { - console.log(`Session data for profile ${getApp().getProfileName()} and session hash ${this.#sessionHash} saved correctly`); + console.log(`Session data for profile ${getApp().getServerManager().getUsername()} and session hash ${this.#sessionHash} saved correctly`); console.log(this.#sessionData); SessionDataChangedEvent.dispatch(this.#sessionData); } else { diff --git a/frontend/react/src/ui/components/olunitlistentry.tsx b/frontend/react/src/ui/components/olunitlistentry.tsx index ce3c63d3..75daaa36 100644 --- a/frontend/react/src/ui/components/olunitlistentry.tsx +++ b/frontend/react/src/ui/components/olunitlistentry.tsx @@ -3,11 +3,31 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { UnitBlueprint } from "../../interfaces"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight"; +import { mode } from "../../other/utils"; -export function OlUnitListEntry(props: { icon: IconProp; blueprint: UnitBlueprint; showCost: boolean; cost: number; onClick: () => void }) { - let pillString = "" as string | undefined - if (props.showCost) pillString = `${props.cost} points` - else pillString = !["aircraft", "helicopter"].includes(props.blueprint.category) ? props.blueprint.type : props.blueprint.abilities +export function OlUnitListEntry(props: { + icon?: IconProp; + silhouette?: string; + blueprint: UnitBlueprint; + showCost: boolean; + cost: number; + onClick: () => void; +}) { + let pillString = "" as string | undefined; + if (props.showCost) pillString = `${props.cost} points`; + else { + if (["aircraft", "helicopter"].includes(props.blueprint.category)) { + let roles = props.blueprint.loadouts?.flatMap((loadout) => loadout.roles).filter((role) => role !== "No task"); + if (roles !== undefined) { + let uniqueRoles = roles?.reduce((acc, current) => {if (!acc.includes(current)) {acc.push(current)} return acc}, [] as string[]) + let mainRole = mode(roles); + pillString = uniqueRoles.length > 6 ? "Multirole" : mainRole; + } + } else { + if (props.blueprint.category) + pillString = props.blueprint.type; + } + } return (
+
+