From 8e20499b921df18e92c96523386f959115b8b9ac Mon Sep 17 00:00:00 2001 From: MarcoJayUsai Date: Fri, 7 Mar 2025 12:00:43 +0100 Subject: [PATCH 01/33] feat(drawings): added navpoints --- .../react/src/map/drawings/drawingsmanager.ts | 69 +++++++++++++++++++ frontend/react/src/map/map.ts | 1 + .../react/src/map/markers/navpointmarker.ts | 48 +++++++++++++ .../src/map/markers/stylesheets/navpoint.css | 27 ++++++++ scripts/lua/backend/OlympusCommand.lua | 24 +++++++ 5 files changed, 169 insertions(+) create mode 100644 frontend/react/src/map/markers/navpointmarker.ts create mode 100644 frontend/react/src/map/markers/stylesheets/navpoint.css diff --git a/frontend/react/src/map/drawings/drawingsmanager.ts b/frontend/react/src/map/drawings/drawingsmanager.ts index 6d193d40..9dc21c32 100644 --- a/frontend/react/src/map/drawings/drawingsmanager.ts +++ b/frontend/react/src/map/drawings/drawingsmanager.ts @@ -3,6 +3,7 @@ import { getApp } from "../../olympusapp"; import { DrawingsInitEvent, DrawingsUpdatedEvent, MapOptionsChangedEvent, SessionDataLoadedEvent } from "../../events"; import { MapOptions } from "../../types/types"; import { Circle, DivIcon, Layer, LayerGroup, layerGroup, Marker, Polygon, Polyline } from "leaflet"; +import { NavpointMarker } from "../markers/navpointmarker"; export abstract class DCSDrawing { #name: string; @@ -452,6 +453,57 @@ export class DCSTextBox extends DCSDrawing { } } +export class DCSNavpoint extends DCSDrawing { + #point: NavpointMarker; + + constructor(drawingData, parent) { + super(drawingData, parent); + + this.#point = new NavpointMarker([drawingData.lat, drawingData.lng], drawingData.callsignStr, drawingData.comment); + + this.setVisibility(true); + } + + getLayer() { + return this.#point; + } + + getLabelLayer() { + return this.#point; + } + + setOpacity(opacity: number): void { + if (opacity === this.#point.options.opacity) return; + + this.#point.options.opacity = opacity; + + /* Hack to force marker redraw */ + const originalVisibility = this.getVisibility(); + this.setVisibility(false); + this.setVisibility(originalVisibility); + + getApp().getDrawingsManager().requestUpdateEventDispatch(); + } + + setVisibility(visibility: boolean): void { + if (visibility && !this.getParent().getLayerGroup().hasLayer(this.#point)) this.#point.addTo(this.getParent().getLayerGroup()); + //@ts-ignore Leaflet typings are wrong + if (!visibility && this.getParent().getLayerGroup().hasLayer(this.#point)) this.#point.removeFrom(this.getParent().getLayerGroup()); + + if (visibility && !this.getParent().getVisibility()) this.getParent().setVisibility(true); + + getApp().getDrawingsManager().requestUpdateEventDispatch(); + } + + getOpacity(): number { + return this.#point.options.opacity ?? 1; + } + + getVisibility(): boolean { + return this.getParent().getLayerGroup().hasLayer(this.#point); + } +} + export class DCSDrawingsContainer { #drawings: DCSDrawing[] = []; #subContainers: DCSDrawingsContainer[] = []; @@ -475,6 +527,9 @@ export class DCSDrawingsContainer { initFromData(drawingsData) { let hasContainers = false; Object.keys(drawingsData).forEach((layerName: string) => { + if (layerName === 'navpoints') { + return; + } if (drawingsData[layerName]["name"] === undefined) { const newContainer = new DCSDrawingsContainer(layerName, this); this.addSubContainer(newContainer); @@ -487,6 +542,7 @@ export class DCSDrawingsContainer { Object.keys(drawingsData).forEach((layerName: string) => { const primitiveType = drawingsData[layerName]["primitiveType"]; + const isANavpoint = !!drawingsData[layerName]['callsignStr']; // Possible primitives: // "Line","TextBox","Polygon","Icon" @@ -499,6 +555,12 @@ export class DCSDrawingsContainer { let newDrawing = new DCSEmptyLayer(drawingsData[layerName], othersContainer) as DCSDrawing; + if (isANavpoint) { + newDrawing = new DCSNavpoint(drawingsData[layerName], othersContainer); + this.addDrawing(newDrawing); + return; + } + switch (primitiveType) { case "Polygon": newDrawing = new DCSPolygon(drawingsData[layerName], othersContainer); @@ -521,6 +583,12 @@ export class DCSDrawingsContainer { if (othersContainer.getDrawings().length === 0) this.removeSubContainer(othersContainer); // Remove empty container } + initNavpoints(drawingsData) { + const newContainer = new DCSDrawingsContainer('Navpoints', this); + this.addSubContainer(newContainer); + newContainer.initFromData(drawingsData); + } + getLayerGroup() { return this.#layerGroup; } @@ -655,6 +723,7 @@ export class DrawingsManager { initDrawings(data: { drawings: Record> }): boolean { if (data && data.drawings) { this.#drawingsContainer.initFromData(data.drawings); + if (data.drawings.navpoints) this.#drawingsContainer.initNavpoints(data.drawings.navpoints); if (this.#sessionDataDrawings["Mission drawings"]) this.#drawingsContainer.fromJSON(this.#sessionDataDrawings["Mission drawings"]); DrawingsInitEvent.dispatch(this.#drawingsContainer); this.#initialized = true; diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 1eb296da..bad1cb36 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -37,6 +37,7 @@ import "./markers/stylesheets/bullseye.css"; import "./markers/stylesheets/units.css"; import "./markers/stylesheets/spot.css"; import "./markers/stylesheets/measure.css"; +import "./markers/stylesheets/navpoint.css"; import "./stylesheets/map.css"; import { initDraggablePath } from "./coalitionarea/draggablepath"; diff --git a/frontend/react/src/map/markers/navpointmarker.ts b/frontend/react/src/map/markers/navpointmarker.ts new file mode 100644 index 00000000..f7092794 --- /dev/null +++ b/frontend/react/src/map/markers/navpointmarker.ts @@ -0,0 +1,48 @@ +import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet"; +import { CustomMarker } from "./custommarker"; + +export class NavpointMarker extends CustomMarker { + #callsignStr: string; + #comment: string; + + constructor(latlng: LatLngExpression, callsignStr: string, comment?: string) { + super(latlng, { interactive: false, draggable: false }); + this.#callsignStr = callsignStr; + comment ? this.#comment = comment : null; + } + + createIcon() { + /* Set the icon */ + let icon = new DivIcon({ + className: "leaflet-navpoint-icon", + iconAnchor: [0, 0], + iconSize: [50, 50], + }); + this.setIcon(icon); + + let el = document.createElement("div"); + el.classList.add("navpoint"); + + // Main icon + let pointIcon = document.createElement("div"); + pointIcon.classList.add("navpoint-icon"); + el.append(pointIcon); + + // Label + let mainLabel: HTMLDivElement = document.createElement("div");; + mainLabel.classList.add("navpoint-main-label"); + mainLabel.innerText = this.#callsignStr; + el.append(mainLabel); + + // Further description + if (this.#comment) { + let commentBox: HTMLDivElement = document.createElement("div");; + commentBox.classList.add("navpoint-comment-box"); + commentBox.innerText = this.#comment; + mainLabel.append(commentBox); + } + + this.getElement()?.appendChild(el); + this.getElement()?.classList.add("ol-navpoint-marker"); + } +} diff --git a/frontend/react/src/map/markers/stylesheets/navpoint.css b/frontend/react/src/map/markers/stylesheets/navpoint.css new file mode 100644 index 00000000..a3e30eb9 --- /dev/null +++ b/frontend/react/src/map/markers/stylesheets/navpoint.css @@ -0,0 +1,27 @@ +.ol-navpoint-marker>.navpoint { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; +} +.ol-navpoint-marker>.navpoint>.navpoint-icon { + height: 8px; + width: 8px; + background: white; + flex: none; + transform: rotate3d(0, 0, 1, 45deg); +} + +.ol-navpoint-marker>.navpoint>.navpoint-main-label { + display: flex; + flex-direction: column; + font-size: 10px; + color: white; +} + +.ol-navpoint-marker .navpoint-comment-box { + font-size: 8px; + font-style: italic; + color: white; + max-width: 50px; +} \ No newline at end of file diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua index c3d68cef..12377fad 100644 --- a/scripts/lua/backend/OlympusCommand.lua +++ b/scripts/lua/backend/OlympusCommand.lua @@ -1076,6 +1076,26 @@ function getUnitDescription(unit) return unit:getDescr() end +-- This function gets the navpoints from the DCS mission +function Olympus.getNavPoints() + local navpoints = {} + if mist.DBs.navPoints ~= nil then + for coalitionName, coalitionNavpoints in pairs(mist.DBs.navPoints) do + for index, navpointDrawingData in pairs(coalitionNavpoints) do + -- Let's convert DCS coords to lat lon + local vec3 = { x = navpointDrawingData['x'], y = 0, z = navpointDrawingData['y'] } + local lat, lng = coord.LOtoLL(vec3) + navpointDrawingData['lat'] = lat + navpointDrawingData['lng'] = lng + navpointDrawingData['coalition'] = coalitionName + end + navpoints[coalitionName] = coalitionNavpoints + end + end + + return navpoints +end + -- This function is periodically called to collect the data of all the existing drawings in the mission to be transmitted to the olympus.dll function Olympus.initializeDrawings() local drawings = {} @@ -1129,6 +1149,10 @@ function Olympus.initializeDrawings() end end + local navpoints = Olympus.getNavPoints() + + drawings['navpoints'] = navpoints + Olympus.drawingsByLayer["drawings"] = drawings -- Send the drawings to the DLL From 6e7b5b1cc3c6bcde4027f353d7d5e02a47a3fc55 Mon Sep 17 00:00:00 2001 From: MarcoJayUsai Date: Fri, 7 Mar 2025 12:36:10 +0100 Subject: [PATCH 02/33] feat(navpoints): added navpoint sublayers --- .../react/src/map/drawings/drawingsmanager.ts | 6 +++-- scripts/lua/backend/OlympusCommand.lua | 22 +++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/frontend/react/src/map/drawings/drawingsmanager.ts b/frontend/react/src/map/drawings/drawingsmanager.ts index 9dc21c32..af74ffce 100644 --- a/frontend/react/src/map/drawings/drawingsmanager.ts +++ b/frontend/react/src/map/drawings/drawingsmanager.ts @@ -530,7 +530,7 @@ export class DCSDrawingsContainer { if (layerName === 'navpoints') { return; } - if (drawingsData[layerName]["name"] === undefined) { + if (drawingsData[layerName]["name"] === undefined && drawingsData[layerName]["callsignStr"] === undefined) { const newContainer = new DCSDrawingsContainer(layerName, this); this.addSubContainer(newContainer); newContainer.initFromData(drawingsData[layerName]); @@ -557,7 +557,9 @@ export class DCSDrawingsContainer { if (isANavpoint) { newDrawing = new DCSNavpoint(drawingsData[layerName], othersContainer); - this.addDrawing(newDrawing); + if (hasContainers) othersContainer.addDrawing(newDrawing); + else this.addDrawing(newDrawing); + if (othersContainer.getDrawings().length === 0) this.removeSubContainer(othersContainer); // Remove empty container return; } diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua index 12377fad..af9f1ce9 100644 --- a/scripts/lua/backend/OlympusCommand.lua +++ b/scripts/lua/backend/OlympusCommand.lua @@ -1077,19 +1077,37 @@ function getUnitDescription(unit) end -- This function gets the navpoints from the DCS mission -function Olympus.getNavPoints() +function Olympus.getNavPoints() + local function extract_tag(str) + return str:match("^%[(.-)%]") + end + local navpoints = {} if mist.DBs.navPoints ~= nil then for coalitionName, coalitionNavpoints in pairs(mist.DBs.navPoints) do + if navpoints[coalitionName] == nil then + navpoints[coalitionName] = {} + end + for index, navpointDrawingData in pairs(coalitionNavpoints) do + local navpointCustomLayer = extract_tag(navpointDrawingData['callsignStr']); + -- Let's convert DCS coords to lat lon local vec3 = { x = navpointDrawingData['x'], y = 0, z = navpointDrawingData['y'] } local lat, lng = coord.LOtoLL(vec3) navpointDrawingData['lat'] = lat navpointDrawingData['lng'] = lng navpointDrawingData['coalition'] = coalitionName + + if navpointCustomLayer ~= nil then + if navpoints[coalitionName][navpointCustomLayer] == nil then + navpoints[coalitionName][navpointCustomLayer] = {} + end + navpoints[coalitionName][navpointCustomLayer][navpointDrawingData['callsignStr']] = navpointDrawingData + else + navpoints[coalitionName][navpointDrawingData['callsignStr']] = navpointDrawingData + end end - navpoints[coalitionName] = coalitionNavpoints end end From 42cfb36c040f0e24e5ce9613b3f3c3431e0a0f25 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Fri, 7 Mar 2025 14:45:01 +0100 Subject: [PATCH 03/33] Added checkbox for autoconnection when local --- frontend/react/src/interfaces.ts | 5 ++++- manager/ejs/expertsettings.ejs | 7 +++++++ manager/ejs/passwords.ejs | 7 +++++++ manager/javascripts/dcsinstance.js | 2 ++ manager/javascripts/filesystem.js | 1 + manager/javascripts/manager.js | 18 +++++++++++++++++- 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index d8f892de..cbf4d021 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -2,6 +2,7 @@ import { LatLng } from "leaflet"; import { Coalition, MapOptions } from "./types/types"; export interface OlympusConfig { + /* Set by user */ frontend: { port: number; customAuthHeaders: { @@ -33,13 +34,15 @@ export interface OlympusConfig { WSEndpoint?: string; }; controllers: [{ type: string; coalition: Coalition; frequency: number; modulation: number; callsign: string }]; - local: boolean; profiles?: { [key: string]: ProfileOptions }; authentication?: { gameMasterPassword: string; blueCommanderPasword: string; redCommanderPassword: string; }; + + /* Set by server */ + local: boolean; } export interface SessionData { diff --git a/manager/ejs/expertsettings.ejs b/manager/ejs/expertsettings.ejs index fc44d79b..8d3b49df 100644 --- a/manager/ejs/expertsettings.ejs +++ b/manager/ejs/expertsettings.ejs @@ -80,6 +80,13 @@ title="Install the camera control plugin, which allows direct control of the DCS camera from Olympus. It is necessary even to control the camera even if Olympus is being used remotely using a browser."> +
+ +
Autoconnect when local + +
+
diff --git a/manager/ejs/passwords.ejs b/manager/ejs/passwords.ejs index c4ad8ca2..d5dcc90b 100644 --- a/manager/ejs/passwords.ejs +++ b/manager/ejs/passwords.ejs @@ -36,5 +36,12 @@
Note: to keep the old passwords, click Next without editing any value.
+
+ +
Autoconnect when local + +
+
diff --git a/manager/javascripts/dcsinstance.js b/manager/javascripts/dcsinstance.js index c5fea078..9be3aa89 100644 --- a/manager/javascripts/dcsinstance.js +++ b/manager/javascripts/dcsinstance.js @@ -149,6 +149,7 @@ class DCSInstance { gameMasterPasswordEdited = false; blueCommanderPasswordEdited = false; redCommanderPasswordEdited = false; + autoconnectWhenLocal = false; constructor(folder) { this.folder = folder; @@ -184,6 +185,7 @@ class DCSInstance { this.backendPort = config["backend"]["port"]; this.backendAddress = config["backend"]["address"]; this.gameMasterPasswordHash = config["authentication"]["gameMasterPassword"]; + this.autoconnectWhenLocal = config["frontend"]["autoconnectWhenLocal"]; this.gameMasterPasswordEdited = false; this.blueCommanderPasswordEdited = false; diff --git a/manager/javascripts/filesystem.js b/manager/javascripts/filesystem.js index 80cd7e21..7cffc58f 100644 --- a/manager/javascripts/filesystem.js +++ b/manager/javascripts/filesystem.js @@ -161,6 +161,7 @@ async function applyConfiguration(folder, instance) { /* Apply the configuration */ config["frontend"]["port"] = instance.frontendPort; + config["frontend"]["autoconnectWhenLocal"] = instance.autoconnectWhenLocal; config["backend"]["port"] = instance.backendPort; config["backend"]["address"] = instance.backendAddress; diff --git a/manager/javascripts/manager.js b/manager/javascripts/manager.js index 5f2558b1..b753c9bf 100644 --- a/manager/javascripts/manager.js +++ b/manager/javascripts/manager.js @@ -332,8 +332,10 @@ class Manager { async onInstallTypeClicked(type) { this.typePage.getElement().querySelector(`.singleplayer`).classList.toggle("selected", type === 'singleplayer'); this.typePage.getElement().querySelector(`.multiplayer`).classList.toggle("selected", type === 'multiplayer'); - if (this.getActiveInstance()) + if (this.getActiveInstance()) { this.getActiveInstance().installationType = type; + this.getActiveInstance().autoconnectWhenLocal = type === 'singleplayer'; + } else { showErrorPopup(`
A critical error occurred!
Check ${this.getLogLocation()} for more info.
`); } @@ -399,6 +401,7 @@ class Manager { this.activePage.hide(); this.connectionsPage.show(); (this.getMode() === 'basic' ? this.connectionsPage : this.expertSettingsPage).getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.getActiveInstance().backendAddress === '*') + (this.getMode() === 'basic' ? this.passwordsPage : this.expertSettingsPage).getElement().querySelector(".autoconnect .checkbox").classList.toggle("checked", this.getActiveInstance().autoconnectWhenLocal) } } else { showErrorPopup(`
A critical error occurred!
Check ${this.getLogLocation()} for more info.
`) @@ -547,6 +550,19 @@ class Manager { } } + async onEnableAutoconnectClicked() { + if (this.getActiveInstance()) { + if (this.getActiveInstance().autoconnectWhenLocal) { + this.getActiveInstance().autoconnectWhenLocal = false; + } else { + this.getActiveInstance().autoconnectWhenLocal = true; + } + this.expertSettingsPage.getElement().querySelector(".autoconnect .checkbox").classList.toggle("checked", this.getActiveInstance().autoconnectWhenLocal) + } else { + showErrorPopup(`
A critical error occurred!
Check ${this.getLogLocation()} for more info.
`) + } + } + /* When the "Return to manager" button is pressed */ async onReturnClicked() { await this.reload(); From be879e3660ff2b6544b086f89e2203ac21546a98 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Fri, 7 Mar 2025 16:14:42 +0100 Subject: [PATCH 04/33] feat: added ability to change and check SRS port in manager --- manager/ejs/connections.ejs | 10 ++++++++++ manager/ejs/expertsettings.ejs | 10 ++++++++++ manager/ejs/instances.ejs | 4 ++++ manager/javascripts/dcsinstance.js | 2 ++ manager/javascripts/filesystem.js | 1 + manager/javascripts/manager.js | 5 +++++ 6 files changed, 32 insertions(+) diff --git a/manager/ejs/connections.ejs b/manager/ejs/connections.ejs index 5c33209c..59ab59f1 100644 --- a/manager/ejs/connections.ejs +++ b/manager/ejs/connections.ejs @@ -44,6 +44,16 @@ +
+ SRS port + + +
+ " + onchange="signal('onSRSPortChanged', this.value)"> +
+
Enable direct backend API connection diff --git a/manager/ejs/expertsettings.ejs b/manager/ejs/expertsettings.ejs index 8d3b49df..f6a55080 100644 --- a/manager/ejs/expertsettings.ejs +++ b/manager/ejs/expertsettings.ejs @@ -66,6 +66,16 @@
+
+ SRS port + + +
+ " + onchange="signal('onSRSPortChanged', this.value)"> +
+
Enable direct backend API connection diff --git a/manager/ejs/instances.ejs b/manager/ejs/instances.ejs index 4c3d707b..3936546c 100644 --- a/manager/ejs/instances.ejs +++ b/manager/ejs/instances.ejs @@ -92,6 +92,10 @@
Backend address
<%= instances[i].installed? instances[i].backendAddress: "N/A" %>
+
+
SRS port
+
<%= instances[i].installed? instances[i].SRSPort: "N/A" %>
+
diff --git a/manager/javascripts/dcsinstance.js b/manager/javascripts/dcsinstance.js index 9be3aa89..e93767b1 100644 --- a/manager/javascripts/dcsinstance.js +++ b/manager/javascripts/dcsinstance.js @@ -150,6 +150,7 @@ class DCSInstance { blueCommanderPasswordEdited = false; redCommanderPasswordEdited = false; autoconnectWhenLocal = false; + SRSPort = 5002; constructor(folder) { this.folder = folder; @@ -186,6 +187,7 @@ class DCSInstance { this.backendAddress = config["backend"]["address"]; this.gameMasterPasswordHash = config["authentication"]["gameMasterPassword"]; this.autoconnectWhenLocal = config["frontend"]["autoconnectWhenLocal"]; + this.SRSPort = config["audio"]["SRSPort"]; this.gameMasterPasswordEdited = false; this.blueCommanderPasswordEdited = false; diff --git a/manager/javascripts/filesystem.js b/manager/javascripts/filesystem.js index 7cffc58f..ae2f1a59 100644 --- a/manager/javascripts/filesystem.js +++ b/manager/javascripts/filesystem.js @@ -164,6 +164,7 @@ async function applyConfiguration(folder, instance) { config["frontend"]["autoconnectWhenLocal"] = instance.autoconnectWhenLocal; config["backend"]["port"] = instance.backendPort; config["backend"]["address"] = instance.backendAddress; + config["audio"]["SRSPort"] = instance.SRSPort; if (instance.gameMasterPassword !== "") config["authentication"]["gameMasterPassword"] = sha256(instance.gameMasterPassword); diff --git a/manager/javascripts/manager.js b/manager/javascripts/manager.js index b753c9bf..451aa617 100644 --- a/manager/javascripts/manager.js +++ b/manager/javascripts/manager.js @@ -517,6 +517,11 @@ class Manager { this.setPort('backend', Number(value)); } + /* When the srs port input value is changed */ + async onSRSPortChanged(value) { + this.getActiveInstance().SRSPort = Number(value); + } + /* When the "Enable API connection" checkbox is clicked */ async onEnableAPIClicked() { if (this.getActiveInstance()) { From f1fb3073d2ede1df890d2b1c2eed729ae88ed727 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Fri, 7 Mar 2025 16:14:45 +0100 Subject: [PATCH 05/33] Update settings.ejs --- manager/ejs/settings.ejs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manager/ejs/settings.ejs b/manager/ejs/settings.ejs index 40b7ab08..9becd035 100644 --- a/manager/ejs/settings.ejs +++ b/manager/ejs/settings.ejs @@ -69,6 +69,10 @@
Backend address
<%= instances[i].installed? instances[i].backendAddress: "N/A" %>
+
+
SRS port
+
<%= instances[i].installed? instances[i].SRSPort: "N/A" %>
+
); break; + case WarningSubstate.ERROR_UPLOADING_CONFIG: + warningText = ( +
+ An error has occurred uploading the admin configuration. + + +
+ ); + break; default: break; } diff --git a/frontend/react/src/ui/panels/drawingmenu.tsx b/frontend/react/src/ui/panels/drawingmenu.tsx index f061e168..d3ea79ba 100644 --- a/frontend/react/src/ui/panels/drawingmenu.tsx +++ b/frontend/react/src/ui/panels/drawingmenu.tsx @@ -302,7 +302,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { > Automatic IADS generation - + {types.map((type, idx) => { if (!(type in typesSelection)) { typesSelection[type] = true; @@ -323,7 +323,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { ); })} - + {eras.map((era) => { if (!(era in erasSelection)) { erasSelection[era] = true; @@ -344,7 +344,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { ); })} - + {["Short range", "Medium range", "Long range"].map((range) => { if (!(range in rangesSelection)) { rangesSelection[range] = true; diff --git a/frontend/react/src/ui/panels/optionsmenu.tsx b/frontend/react/src/ui/panels/optionsmenu.tsx index e98df899..53946a1c 100644 --- a/frontend/react/src/ui/panels/optionsmenu.tsx +++ b/frontend/react/src/ui/panels/optionsmenu.tsx @@ -9,15 +9,18 @@ import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent import { OlAccordion } from "../components/olaccordion"; import { Shortcut } from "../../shortcut/shortcut"; import { OlSearchBar } from "../components/olsearchbar"; -import { FaTrash, FaXmark } from "react-icons/fa6"; +import { FaTrash, FaUserGroup, FaXmark } from "react-icons/fa6"; import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; -import { FaQuestionCircle } from "react-icons/fa"; +import { FaCog, FaKey, FaPlus, FaQuestionCircle } from "react-icons/fa"; +import { sha256 } from "js-sha256"; +import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; const enum Accordion { NONE, BINDINGS, MAP_OPTIONS, CAMERA_PLUGIN, + ADMIN, } export function OptionsMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { @@ -25,6 +28,31 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut }); const [openAccordion, setOpenAccordion] = useState(Accordion.NONE); const [filterString, setFilterString] = useState(""); + const [admin, setAdmin] = useState(false); + const [password, setPassword] = useState(""); + + const checkPassword = (password: string) => { + var hash = sha256.create(); + + const requestOptions: RequestInit = { + method: "GET", // Specify the request method + headers: { + Authorization: "Basic " + btoa(`Admin:${hash.update(password).hex()}`), + }, // Specify the content type + }; + + fetch(`./admin/config`, requestOptions) + .then((response) => { + if (response.status === 200) { + console.log(`Admin password correct`); + getApp().setAdminPassword(password); + getApp().setState(OlympusState.ADMIN) + return response.json(); + } else { + throw new Error("Admin password incorrect"); + } + }) + }; useEffect(() => { MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); @@ -186,18 +214,14 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre }} >
-
- {}} coalition={mapOptions.AWACSCoalition} /> - Coalition of unit bullseye info -
-
- {" "} -
- Change the coalition of the bullseye to use to provide bullseye information in the unit tooltip. +
+ {}} coalition={mapOptions.AWACSCoalition} /> + Coalition of unit bullseye info +
+
+ {" "} +
Change the coalition of the bullseye to use to provide bullseye information in the unit tooltip.
-
@@ -207,12 +231,6 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre open={openAccordion === Accordion.CAMERA_PLUGIN} title="Camera plugin options" > -
void; childre
-
+ + +
+
+ + { + setPassword(ev.currentTarget.value); + }} + className={` + max-w-44 rounded-lg border border-gray-300 bg-gray-50 p-2.5 + text-sm text-gray-900 + dark:border-gray-600 dark:bg-gray-700 dark:text-white + dark:placeholder-gray-400 dark:focus:border-blue-500 + dark:focus:ring-blue-500 + focus:border-blue-500 focus:ring-blue-500 + `} + placeholder="Enter password" + required + /> +
-
diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index 21674db1..3f95e09d 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -32,6 +32,7 @@ import { ServerOverlay } from "./serveroverlay"; import { ImportExportModal } from "./modals/importexportmodal"; import { WarningModal } from "./modals/warningmodal"; import { TrainingModal } from "./modals/trainingmodal"; +import { AdminModal } from "./modals/adminmodal"; export function UI() { const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); @@ -75,6 +76,7 @@ export function UI() { + )} diff --git a/frontend/server/package.json b/frontend/server/package.json index af228941..0c1daaf5 100644 --- a/frontend/server/package.json +++ b/frontend/server/package.json @@ -4,7 +4,8 @@ "version": "{{OLYMPUS_VERSION_NUMBER}}", "scripts": { "build-release": "call ./scripts/build-release.bat", - "server": "electron . --server", + "server-electron": "electron . --server", + "server": "node ./build/www.js", "client": "electron .", "tsc": "tsc" }, diff --git a/frontend/server/src/app.ts b/frontend/server/src/app.ts index eab4d934..b74b57e9 100644 --- a/frontend/server/src/app.ts +++ b/frontend/server/src/app.ts @@ -51,6 +51,7 @@ module.exports = function (configLocation, viteProxy) { /* Config specific routers */ const elevationRouter = require("./routes/api/elevation")(configLocation); const resourcesRouter = require("./routes/resources")(configLocation); + const adminRouter = require("./routes/admin")(configLocation); /* Default routers */ const airbasesRouter = require("./routes/api/airbases"); @@ -113,6 +114,9 @@ module.exports = function (configLocation, viteProxy) { "Blue commander": config["authentication"]["blueCommanderPassword"], "Red commander": config["authentication"]["redCommanderPassword"], }; + if (config["authentication"]["adminPassword"]) { + defaultUsers["Admin"] = config["authentication"]["adminPassword"]; + } let users = {}; Object.keys(usersConfig).forEach( (user) => (users[user] = usersConfig[user].password) @@ -122,7 +126,9 @@ module.exports = function (configLocation, viteProxy) { }); /* Define middleware */ - app.use(logger("dev")); + app.use(logger('dev', { + skip: function (req, res) { return res.statusCode < 400 } + })); /* Authorization middleware */ if ( @@ -238,6 +244,9 @@ module.exports = function (configLocation, viteProxy) { app.use("/api/speech", speechRouter); app.use("/resources", resourcesRouter); + app.use("/admin", auth); + app.use("/admin", adminRouter); + /* Set default index */ if (viteProxy) { app.use( diff --git a/frontend/server/src/routes/admin.ts b/frontend/server/src/routes/admin.ts new file mode 100644 index 00000000..d39a6a99 --- /dev/null +++ b/frontend/server/src/routes/admin.ts @@ -0,0 +1,100 @@ +import express = require("express"); +import fs = require("fs"); +import path = require("path"); + +const router = express.Router(); + +module.exports = function (configLocation) { + router.get("/config", function (req, res, next) { + if (req.auth?.user === "Admin") { + /* Read the users configuration file */ + let usersConfig = {}; + if ( + fs.existsSync( + path.join(path.dirname(configLocation), "olympusUsers.json") + ) + ) { + let rawdata = fs.readFileSync( + path.join(path.dirname(configLocation), "olympusUsers.json"), + { encoding: "utf-8" } + ); + usersConfig = JSON.parse(rawdata); + } + + /* Read the groups configuration file */ + let groupsConfig = {}; + if ( + fs.existsSync( + path.join(path.dirname(configLocation), "olympusGroups.json") + ) + ) { + let rawdata = fs.readFileSync( + path.join(path.dirname(configLocation), "olympusGroups.json"), + { encoding: "utf-8" } + ); + groupsConfig = JSON.parse(rawdata); + } + + res.send({ users: usersConfig, groups: groupsConfig }); + res.end(); + } else { + res.sendStatus(401); + } + }); + + router.put("/config", function (req, res, next) { + if (req.auth?.user === "Admin") { + /* Create a backup folder for the configuration files */ + let backupFolder = path.join(path.dirname(configLocation), "Olympus Configs Backup"); + if (!fs.existsSync(backupFolder)) { + fs.mkdirSync(backupFolder); + } + + /* Make a backup of the existing files */ + let timestamp = new Date().toISOString().replace(/:/g, "-"); + fs.copyFileSync( + path.join(path.dirname(configLocation), "olympusUsers.json"), + path.join( + path.dirname(configLocation), + "Olympus Configs Backup", + "olympusUsers.json." + timestamp + ) + ); + fs.copyFileSync( + path.join(path.dirname(configLocation), "olympusGroups.json"), + path.join( + path.dirname(configLocation), + "Olympus Configs Backup", + "olympusGroups.json." + timestamp + ) + ); + + /* Save the users configuration file */ + let usersConfig = req.body.users; + + if (usersConfig) { + fs.writeFileSync( + path.join(path.dirname(configLocation), "olympusUsers.json"), + JSON.stringify(usersConfig, null, 2) + ); + } + + /* Save the groups configuration file */ + let groupsConfig = req.body.groups; + + if (groupsConfig) { + fs.writeFileSync( + path.join(path.dirname(configLocation), "olympusGroups.json"), + JSON.stringify(groupsConfig, null, 2) + ); + } + + res.send({ users: usersConfig, groups: groupsConfig }); + res.end(); + } else { + res.sendStatus(401); + } + }); + + return router; +}; diff --git a/manager/ejs/expertsettings.ejs b/manager/ejs/expertsettings.ejs index f6a55080..48679ac2 100644 --- a/manager/ejs/expertsettings.ejs +++ b/manager/ejs/expertsettings.ejs @@ -19,19 +19,25 @@ Game Master Password - "> + ">
Blue Commander Password - "> + ">
Red Commander Password - "> + "> +
+
+ Admin Password + + ">
" style="color: var(--offwhite); font-size: var(--normal); color: var(--lightgray);"> Note: to keep the old passwords, click Next without editing any value. diff --git a/manager/ejs/passwords.ejs b/manager/ejs/passwords.ejs index d5dcc90b..15c8bd5b 100644 --- a/manager/ejs/passwords.ejs +++ b/manager/ejs/passwords.ejs @@ -1,5 +1,19 @@
@@ -15,33 +29,43 @@
+
Game Master Password - "> + ">
Blue Commander Password - "> + ">
Red Commander Password - "> + ">
Note: to keep the old passwords, click Next without editing any value.
-
Autoconnect when local +
Autoconnect when local
+
+
+
+ Admin Password + + "> +
+
diff --git a/manager/javascripts/dcsinstance.js b/manager/javascripts/dcsinstance.js index 3299d0f5..9aa93fd1 100644 --- a/manager/javascripts/dcsinstance.js +++ b/manager/javascripts/dcsinstance.js @@ -136,6 +136,7 @@ class DCSInstance { blueCommanderPassword = ""; redCommanderPassword = ""; gameMasterPasswordHash = ""; + adminPassword = ""; installed = false; error = false; webserverOnline = false; @@ -149,6 +150,7 @@ class DCSInstance { gameMasterPasswordEdited = false; blueCommanderPasswordEdited = false; redCommanderPasswordEdited = false; + adminPasswordEdited = false; autoconnectWhenLocal = false; SRSPort = 5002; @@ -196,6 +198,7 @@ class DCSInstance { this.gameMasterPasswordEdited = false; this.blueCommanderPasswordEdited = false; this.redCommanderPasswordEdited = false; + this.adminPasswordEdited = false; } catch (err) { showErrorPopup(`
A critical error has occurred while reading your Olympus configuration file.
Please manually reinstall Olympus in ${this.folder} using either the installation Wizard or the Expert view.
`) @@ -277,7 +280,7 @@ class DCSInstance { /** Set Blue Commander password * - * @param {String} newAddress The new Blue Commander password to set + * @param {String} newPassword The new Blue Commander password to set */ setBlueCommanderPassword(newPassword) { this.blueCommanderPassword = newPassword; @@ -286,13 +289,22 @@ class DCSInstance { /** Set Red Commander password * - * @param {String} newAddress The new Red Commander password to set + * @param {String} newPassword The new Red Commander password to set */ setRedCommanderPassword(newPassword) { this.redCommanderPassword = newPassword; this.redCommanderPasswordEdited = true; } + /** Set Admin password + * + * @param {String} newPassword The new Admin password to set + */ + setAdminPassword(newPassword) { + this.adminPassword = newPassword; + this.adminPasswordEdited = true; + } + /** Checks if any password has been edited by the user * * @returns true if any password was edited @@ -306,7 +318,10 @@ class DCSInstance { * @returns true if all the password have been set */ arePasswordsSet() { - return !(getManager().getActiveInstance().gameMasterPassword === '' || getManager().getActiveInstance().blueCommanderPassword === '' || getManager().getActiveInstance().redCommanderPassword === ''); + if (getManager().getActiveInstance().installationType === "singleplayer") + return !(getManager().getActiveInstance().gameMasterPassword === '' || getManager().getActiveInstance().blueCommanderPassword === '' || getManager().getActiveInstance().redCommanderPassword === ''); + else + return !(getManager().getActiveInstance().gameMasterPassword === '' || getManager().getActiveInstance().blueCommanderPassword === '' || getManager().getActiveInstance().redCommanderPassword === '' || getManager().getActiveInstance().adminPassword === ''); } /** Checks if all the passwords are different diff --git a/manager/javascripts/filesystem.js b/manager/javascripts/filesystem.js index 4e10048d..6bea517b 100644 --- a/manager/javascripts/filesystem.js +++ b/manager/javascripts/filesystem.js @@ -181,6 +181,9 @@ async function applyConfiguration(folder, instance) { if (instance.redCommanderPassword !== "") config["authentication"]["redCommanderPassword"] = sha256(instance.redCommanderPassword); + if (instance.adminPassword !== "") + config["authentication"]["adminPassword"] = sha256(instance.adminPassword); + await fsp.writeFile(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4)); logger.log(`Config succesfully applied in ${folder}`) } else { diff --git a/manager/javascripts/manager.js b/manager/javascripts/manager.js index 451aa617..3bcbedb3 100644 --- a/manager/javascripts/manager.js +++ b/manager/javascripts/manager.js @@ -475,7 +475,7 @@ class Manager { } async onGameMasterPasswordChanged(value) { - for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) { + for (let input of this.activePage.getElement().querySelectorAll("input[type='password'].unique")) { input.placeholder = ""; } @@ -486,7 +486,7 @@ class Manager { } async onBlueCommanderPasswordChanged(value) { - for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) { + for (let input of this.activePage.getElement().querySelectorAll("input[type='password'].unique")) { input.placeholder = ""; } @@ -497,7 +497,7 @@ class Manager { } async onRedCommanderPasswordChanged(value) { - for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) { + for (let input of this.activePage.getElement().querySelectorAll("input[type='password'].unique")) { input.placeholder = ""; } @@ -507,6 +507,13 @@ class Manager { showErrorPopup(`
A critical error occurred!
Check ${this.getLogLocation()} for more info.
`); } + async onAdminPasswordChanged(value) { + if (this.getActiveInstance()) + this.getActiveInstance().setAdminPassword(value); + else + showErrorPopup(`
A critical error occurred!
Check ${this.getLogLocation()} for more info.
`); + } + /* When the frontend port input value is changed */ async onFrontendPortChanged(value) { this.setPort('frontend', Number(value)); From 46f2ff44039f9977d0c0ab676f275fd76a1f363e Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Tue, 11 Mar 2025 16:25:16 +0100 Subject: [PATCH 09/33] feat: added proxyHeader and comments to config file --- frontend/server/src/routes/resources.ts | 10 ++++- olympus.json | 49 +++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/frontend/server/src/routes/resources.ts b/frontend/server/src/routes/resources.ts index a8c0f48b..77f79d88 100644 --- a/frontend/server/src/routes/resources.ts +++ b/frontend/server/src/routes/resources.ts @@ -22,9 +22,15 @@ module.exports = function (configLocation) { profiles = JSON.parse(rawdata); } if (fs.existsSync(configLocation)) { + /* Read the config file */ let rawdata = fs.readFileSync(configLocation, "utf-8"); - const local = ["127.0.0.1", "::ffff:127.0.0.1", "::1"].includes(req.connection.remoteAddress); const config = JSON.parse(rawdata); + + /* Check if the connection is local */ + let local = false; + if (config.frontend.autoconnectWhenLocal) + local = req.headers[config.frontend.proxyHeader] === undefined; + let resConfig = { frontend: { ...config.frontend }, audio: { ...(config.audio ?? {}) }, @@ -32,9 +38,11 @@ module.exports = function (configLocation) { profiles: { ...(profiles ?? {}) }, local: local, }; + if (local) { resConfig["authentication"] = config["authentication"] } + res.send( JSON.stringify(resConfig) ); diff --git a/olympus.json b/olympus.json index bc457ea5..e8d24e36 100644 --- a/olympus.json +++ b/olympus.json @@ -1,26 +1,53 @@ { "backend": { + "_comment1": "These are the address and port of the backend server, i.e. the server run by the Olympus dll mod.", + "_comment2": "localhost should be used if the backend is running on the same machine as the frontend server, which is usually the case.", + "_comment3": "If a direct connection is desired, e.g. for API usage, use '*' as address.", + "_comment4": "The desired port should be available and not used by other processes.", + "address": "localhost", "port": 4512 }, "authentication": { - "gameMasterPassword": "4b8823ed9e5c2392ab4a791913bb8ce41956ea32e308b760eefb97536746dd33", - "blueCommanderPassword": "b0ea4230c1558c5313165eda1bdb7fced008ca7f2ca6b823fb4d26292f309098", - "redCommanderPassword": "302bcbaf2a3fdcf175b689bf102d6cdf9328f68a13d4096101bba806482bfed9" + "_comment1": "These are the sha256 hashed passwords for the game master, the two commanders, and the admin. They are used to authenticate the users.", + + "gameMasterPassword": "", + "blueCommanderPassword": "", + "redCommanderPassword": "", + "admin": "" }, "frontend": { + "_comment1": "These are the settings for the frontend server, i.e. the server which hosts the Olympus GUI web interface.", + "_comment2": "The port should be available and not used by other processes and is used to load the interface.", + "port": 3000, "customAuthHeaders": { + "_comment1": "These are the custom headers used for authentication. They are used to authenticate the users and skip the login page", + "_comment2": "If enabled, the frontend server will look for the specified headers in the request and use them to authenticate the user.", + "_comment3": "The username header should contain the username and the group header should contain the group of the user.", + "_comment4": "If the headers are not present or the user is not authenticated, the user will be redirected to the login page.", + "_comment5": "This is useful for integrating Olympus with other systems, e.g. a SSO provider", + "_comment6": "The group should be one of the groups defined using the admin page on the web interface", + "_comment7": "If the user is by default authorized to more than one command mode, x-command-mode header can be used to specify the default command mode", + "_comment8": "Otherwise, the login page will be skipped, but a command more selection page will still be shown", + "enabled": false, "username": "X-Authorized", "group": "X-Group" }, "elevationProvider": { + "_comment1": "The elevation provider is used to fetch elevation data for the map. It should be a URL with {lat} and {lng} placeholders.", + "provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip", "username": null, "password": null }, "mapLayers": { + "_comment1": "These are the map layers used by the frontend server. They are used to display the map in the interface.", + "_comment2": "The urlTemplate should be a URL with {z}, {x}, and {y} placeholders for the zoom level and tile coordinates.", + "_comment3": "The minZoom and maxZoom define the zoom levels at which the layer is visible.", + "_comment4": "The attribution is the text displayed in the bottom right corner of the map.", + "ArcGIS Satellite": { "urlTemplate": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", "minZoom": 1, @@ -35,12 +62,26 @@ } }, "mapMirrors": { + "_comment1": "These are the map mirrors used by the frontend server. They are used to load the map tiles from different sources.", + "_comment2": "The key is the name of the mirror and the value is the URL of the mirror.", + "DCS Map (Official)": "https://maps.dcsolympus.com/maps", "DCS Map (Alt.)": "https://refugees.dcsolympus.com/maps" }, - "autoconnectWhenLocal": true + + "_comment3": "If autoconnectWhenLocal is true, the frontend server will automatically connect to the backend server when running on the same machine.", + "_comment4": "If a proxy is used, the proxyHeader should be set to the header used to forward the client IP address to the backend server.", + "_comment5": "This is useful when running the frontend server behind a reverse proxy, e.g. nginx, allowing to skip login when connecting locally but still authenticate when connecting remotely.", + + "autoconnectWhenLocal": true, + "proxyHeader": "x-forwarded-for" }, "audio": { + "_comment1": "These are the settings for the audio backend, i.e. the service which handles direct connection of Olympus to a SRS server.", + "_comment2": "The SRSPort is the port used to connect to the SRS server and should be set to be the same as the value in SRS (5002 by default).", + "_comment3": "The WSPort is the port used by the web interface to connect to the audio backend WebSocket. It should be available and not used by other processes.", + "_comment4": "The WSEndpoint is the endpoint used by the web interface to connect to the audio backend WebSocket when using a reverse proxy. A websocket proxy should be set up to forward requests from this endpoint to WSPort.", + "SRSPort": 5002, "WSPort": 4000, "WSEndpoint": "audio" From 23f9eee39ff7b34272540df7f2eb6d260e9a4aa3 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Tue, 11 Mar 2025 16:28:26 +0100 Subject: [PATCH 10/33] fix: Fixed error in SRS coalition --- frontend/react/src/audio/audiomanager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index a310fc43..8c4e0094 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -2,7 +2,7 @@ import { AudioMessageType, BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMAN import { MicrophoneSource } from "./microphonesource"; import { RadioSink } from "./radiosink"; import { getApp } from "../olympusapp"; -import { makeID } from "../other/utils"; +import { coalitionToEnum, makeID } from "../other/utils"; import { FileSource } from "./filesource"; import { AudioSource } from "./audiosource"; import { Buffer } from "buffer"; @@ -379,7 +379,7 @@ export class AudioManager { let message = { type: "Settings update", guid: this.#guid, - coalition: this.#coalition, + coalition: coalitionToEnum(this.#coalition), settings: this.#sinks .filter((sink) => sink instanceof RadioSink) .map((radio) => { From 34f9a8bc40a83a0252b786f2286aa2ff1d10ed83 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Wed, 12 Mar 2025 18:43:14 +0100 Subject: [PATCH 11/33] feat: Improvements on scenic modes --- backend/core/include/datatypes.h | 11 + backend/core/include/groundunit.h | 2 +- backend/core/include/unit.h | 33 + backend/core/src/groundunit.cpp | 171 +++-- backend/core/src/scheduler.cpp | 23 + backend/core/src/unit.cpp | 11 + frontend/react/src/constants/constants.ts | 22 +- frontend/react/src/interfaces.ts | 28 +- frontend/react/src/server/servermanager.ts | 32 + .../react/src/ui/components/olnumberinput.tsx | 3 +- .../react/src/ui/panels/unitcontrolmenu.tsx | 720 +++++++++++++++--- frontend/react/src/unit/unit.ts | 120 +++ frontend/react/src/unit/unitsmanager.ts | 61 +- manager/javascripts/filesystem.js | 4 +- notes.txt | 8 + olympus.json | 71 +- 16 files changed, 1093 insertions(+), 227 deletions(-) diff --git a/backend/core/include/datatypes.h b/backend/core/include/datatypes.h index bd99cd6c..d1a2bcb2 100644 --- a/backend/core/include/datatypes.h +++ b/backend/core/include/datatypes.h @@ -54,6 +54,17 @@ namespace DataIndex { racetrackLength, racetrackAnchor, racetrackBearing, + timeToNextTasking, + barrelHeight, + muzzleVelocity, + aimTime, + shotsToFire, + shotsBaseInterval, + shotsBaseScatter, + engagementRange, + targetingRange, + aimMethodRange, + acquisitionRange, lastIndex, endOfData = 255 }; diff --git a/backend/core/include/groundunit.h b/backend/core/include/groundunit.h index b77ef33b..8af19ea6 100644 --- a/backend/core/include/groundunit.h +++ b/backend/core/include/groundunit.h @@ -17,7 +17,7 @@ public: virtual void setOnOff(bool newOnOff, bool force = false); virtual void setFollowRoads(bool newFollowRoads, bool force = false); - void aimAtPoint(Coords aimTarget); + string aimAtPoint(Coords aimTarget); protected: virtual void AIloop(); diff --git a/backend/core/include/unit.h b/backend/core/include/unit.h index 7bbc1397..ce2f1b83 100644 --- a/backend/core/include/unit.h +++ b/backend/core/include/unit.h @@ -112,6 +112,17 @@ public: virtual void setRacetrackLength(double newValue) { updateValue(racetrackLength, newValue, DataIndex::racetrackLength); } virtual void setRacetrackAnchor(Coords newValue) { updateValue(racetrackAnchor, newValue, DataIndex::racetrackAnchor); } virtual void setRacetrackBearing(double newValue) { updateValue(racetrackBearing, newValue, DataIndex::racetrackBearing); } + virtual void setTimeToNextTasking(double newValue) { updateValue(timeToNextTasking, newValue, DataIndex::timeToNextTasking); } + virtual void setBarrelHeight(double newValue) { updateValue(barrelHeight, newValue, DataIndex::barrelHeight); } + virtual void setMuzzleVelocity(double newValue) { updateValue(muzzleVelocity, newValue, DataIndex::muzzleVelocity); } + virtual void setAimTime(double newValue) { updateValue(aimTime, newValue, DataIndex::aimTime); } + virtual void setShotsToFire(unsigned int newValue) { updateValue(shotsToFire, newValue, DataIndex::shotsToFire); } + virtual void setShotsBaseInterval(double newValue) { updateValue(shotsBaseInterval, newValue, DataIndex::shotsBaseInterval); } + virtual void setShotsBaseScatter(double newValue) { updateValue(shotsBaseScatter, newValue, DataIndex::shotsBaseScatter); } + virtual void setEngagementRange(double newValue) { updateValue(engagementRange, newValue, DataIndex::engagementRange); } + virtual void setTargetingRange(double newValue) { updateValue(targetingRange, newValue, DataIndex::targetingRange); } + virtual void setAimMethodRange(double newValue) { updateValue(aimMethodRange, newValue, DataIndex::aimMethodRange); } + virtual void setAcquisitionRange(double newValue) { updateValue(acquisitionRange, newValue, DataIndex::acquisitionRange); } /********** Getters **********/ virtual string getCategory() { return category; }; @@ -163,6 +174,17 @@ public: virtual double getRacetrackLength() { return racetrackLength; } virtual Coords getRacetrackAnchor() { return racetrackAnchor; } virtual double getRacetrackBearing() { return racetrackBearing; } + virtual double getTimeToNextTasking() { return timeToNextTasking; } + virtual double getBarrelHeight() { return barrelHeight; } + virtual double getMuzzleVelocity() { return muzzleVelocity; } + virtual double getAimTime() { return aimTime; } + virtual unsigned int getShotsToFire() { return shotsToFire; } + virtual double getShotsBaseInterval() { return shotsBaseInterval; } + virtual double getShotsBaseScatter() { return shotsBaseScatter; } + virtual double getEngagementRange() { return engagementRange; } + virtual double getTargetingRange() { return targetingRange; } + virtual double getAimMethodRange() { return aimMethodRange; } + virtual double getAcquisitionRange() { return acquisitionRange; } protected: unsigned int ID; @@ -217,6 +239,17 @@ protected: unsigned char shotsScatter = 2; unsigned char shotsIntensity = 2; unsigned char health = 100; + double timeToNextTasking = 0; + double barrelHeight = 1.0; /* m */ + double muzzleVelocity = 860; /* m/s */ + double aimTime = 10; /* s */ + unsigned int shotsToFire = 10; + double shotsBaseInterval = 15; /* s */ + double shotsBaseScatter = 2; /* degs */ + double engagementRange = 10000; /* m */ + double targetingRange = 0; /* m */ + double aimMethodRange = 0; /* m */ + double acquisitionRange = 0; /* m */ /********** Other **********/ unsigned int taskCheckCounter = 0; diff --git a/backend/core/src/groundunit.cpp b/backend/core/src/groundunit.cpp index dff31bf9..194bdfb0 100644 --- a/backend/core/src/groundunit.cpp +++ b/backend/core/src/groundunit.cpp @@ -49,6 +49,31 @@ void GroundUnit::setDefaults(bool force) setROE(ROE::WEAPON_FREE, force); setOnOff(onOff, force); setFollowRoads(followRoads, force); + + /* Load gun values from database */ + if (database.has_object_field(to_wstring(name))) { + json::value databaseEntry = database[to_wstring(name)]; + if (databaseEntry.has_number_field(L"barrelHeight")) + setBarrelHeight(databaseEntry[L"barrelHeight"].as_number().to_double()); + if (databaseEntry.has_number_field(L"muzzleVelocity")) + setMuzzleVelocity(databaseEntry[L"muzzleVelocity"].as_number().to_double()); + if (databaseEntry.has_number_field(L"aimTime")) + setAimTime(databaseEntry[L"aimTime"].as_number().to_double()); + if (databaseEntry.has_number_field(L"shotsToFire")) + setShotsToFire(databaseEntry[L"shotsToFire"].as_number().to_uint32()); + if (databaseEntry.has_number_field(L"engagementRange")) + setEngagementRange(databaseEntry[L"engagementRange"].as_number().to_double()); + if (databaseEntry.has_number_field(L"shotsBaseInterval")) + setShotsBaseInterval(databaseEntry[L"shotsBaseInterval"].as_number().to_double()); + if (databaseEntry.has_number_field(L"shotsBaseScatter")) + setShotsBaseScatter(databaseEntry[L"shotsBaseScatter"].as_number().to_double()); + if (databaseEntry.has_number_field(L"targetingRange")) + setTargetingRange(databaseEntry[L"targetingRange"].as_number().to_double()); + if (databaseEntry.has_number_field(L"aimMethodRange")) + setAimMethodRange(databaseEntry[L"aimMethodRange"].as_number().to_double()); + if (databaseEntry.has_number_field(L"acquisitionRange")) + setAcquisitionRange(databaseEntry[L"acquisitionRange"].as_number().to_double()); + } } void GroundUnit::setState(unsigned char newState) @@ -214,7 +239,7 @@ void GroundUnit::AIloop() break; } case State::SIMULATE_FIRE_FIGHT: { - setTask("Simulating fire fight"); + string taskString = ""; if (internalCounter == 0 && targetPosition != Coords(NULL) && scheduler->getLoad() < 30) { /* Get the distance and bearing to the target */ @@ -229,21 +254,16 @@ void GroundUnit::AIloop() Geodesic::WGS84().Direct(scatteredTargetPosition.lat, scatteredTargetPosition.lng, bearing1 + 90, scatterDistance, scatteredTargetPosition.lat, scatteredTargetPosition.lng); /* Recover the data from the database */ - double aimTime = 2; /* s */ bool indirectFire = false; - double shotsBaseInterval = 15; /* s */ if (database.has_object_field(to_wstring(name))) { json::value databaseEntry = database[to_wstring(name)]; - if (databaseEntry.has_number_field(L"aimTime")) - aimTime = databaseEntry[L"aimTime"].as_number().to_double(); if (databaseEntry.has_boolean_field(L"indirectFire")) indirectFire = databaseEntry[L"indirectFire"].as_bool(); - if (databaseEntry.has_number_field(L"shotsBaseInterval")) - shotsBaseInterval = databaseEntry[L"shotsBaseInterval"].as_number().to_double(); } /* If the unit is of the indirect fire type, like a mortar, simply shoot at the target */ if (indirectFire) { + taskString += "Simulating fire fight with indirect fire"; log(unitName + "(" + name + ")" + " simulating fire fight with indirect fire"); std::ostringstream taskSS; taskSS.precision(10); @@ -254,8 +274,10 @@ void GroundUnit::AIloop() } /* Otherwise use the aim method */ else { + taskString += "Simulating fire fight with aim point method. "; log(unitName + "(" + name + ")" + " simulating fire fight with aim at point method"); - aimAtPoint(scatteredTargetPosition); + string aimTaskString = aimAtPoint(scatteredTargetPosition); + taskString += aimTaskString; } /* Wait an amout of time depending on the shots intensity */ @@ -270,10 +292,15 @@ void GroundUnit::AIloop() internalCounter = static_cast(3 / FRAMERATE_TIME_INTERVAL); internalCounter--; + setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL); + + if (taskString.length() > 0) + setTask(taskString); + break; } case State::SCENIC_AAA: { - setTask("Scenic AAA"); + string taskString = ""; /* Only perform scenic functions when the scheduler is "free" */ if (((!getHasTask() && scheduler->getLoad() < 30) || internalCounter == 0)) { @@ -283,29 +310,42 @@ void GroundUnit::AIloop() Unit* target = unitsManager->getClosestUnit(this, targetCoalition, { "Aircraft", "Helicopter" }, distance); /* Recover the data from the database */ - double aimTime = 2; /* s */ - double shotsBaseInterval = 15; /* s */ + bool flak = false; if (database.has_object_field(to_wstring(name))) { json::value databaseEntry = database[to_wstring(name)]; - if (databaseEntry.has_number_field(L"aimTime")) - aimTime = databaseEntry[L"aimTime"].as_number().to_double(); - if (databaseEntry.has_number_field(L"shotsBaseInterval")) - shotsBaseInterval = databaseEntry[L"shotsBaseInterval"].as_number().to_double(); + if (databaseEntry.has_boolean_field(L"flak")) + flak = databaseEntry[L"flak"].as_bool(); } /* Only run if an enemy air unit is closer than 20km to avoid useless load */ - if (target != nullptr && distance < 20000 /* m */) { + double activationDistance = 20000; + if (2 * engagementRange > activationDistance) + activationDistance = 2 * engagementRange; + + if (target != nullptr && distance < activationDistance /* m */) { double r = 15; /* m */ - double barrelElevation = r * tan(acos(((double)(rand()) / (double)(RAND_MAX)))); + double barrelElevation = position.alt + barrelHeight + r * tan(acos(((double)(rand()) / (double)(RAND_MAX)))); double lat = 0; double lng = 0; double randomBearing = ((double)(rand()) / (double)(RAND_MAX)) * 360; Geodesic::WGS84().Direct(position.lat, position.lng, randomBearing, r, lat, lng); + if (flak) { + lat = position.lat + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 0.01; + lng = position.lng + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 0.01; + barrelElevation = target->getPosition().alt + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 1000; + taskString += "Flak box mode."; + } + else { + taskString += "Scenic AAA. Bearing: " + to_string((int)round(randomBearing)) + "deg"; + } + + taskString += ". Aim point elevation " + to_string((int) round(barrelElevation)) + "m AGL"; + std::ostringstream taskSS; taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << position.alt + barrelElevation << ", radius = 0.001}"; + taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << barrelElevation << ", radius = 0.001, expendQty = " << shotsToFire << " }"; Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); scheduler->appendCommand(command); setHasTask(true); @@ -313,16 +353,26 @@ void GroundUnit::AIloop() /* Wait an amout of time depending on the shots intensity */ internalCounter = static_cast(((ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + aimTime) / FRAMERATE_TIME_INTERVAL); } + else { + if (target == nullptr) + taskString += "Scenic AAA. No valid target."; + else + taskString += "Scenic AAA. Target outside max range: " + to_string((int)round(distance)) + "m."; + } } if (internalCounter == 0) internalCounter = static_cast(3 / FRAMERATE_TIME_INTERVAL); internalCounter--; + setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL); + if (taskString.length() > 0) + setTask(taskString); + break; } case State::MISS_ON_PURPOSE: { - setTask("Missing on purpose"); + string taskString = ""; /* Check that the unit can perform AAA duties */ bool canAAA = false; @@ -340,43 +390,6 @@ void GroundUnit::AIloop() unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition; unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2; - /* Default gun values */ - double barrelHeight = 1.0; /* m */ - double muzzleVelocity = 860; /* m/s */ - double aimTime = 10; /* s */ - unsigned int shotsToFire = 10; - double shotsBaseInterval = 15; /* s */ - double shotsBaseScatter = 2; /* degs */ - double engagementRange = 10000; /* m */ - double targetingRange = 0; /* m */ - double aimMethodRange = 0; /* m */ - double acquisitionRange = 0; /* m */ - - /* Load gun values from database */ - if (database.has_object_field(to_wstring(name))) { - json::value databaseEntry = database[to_wstring(name)]; - if (databaseEntry.has_number_field(L"barrelHeight")) - barrelHeight = databaseEntry[L"barrelHeight"].as_number().to_double(); - if (databaseEntry.has_number_field(L"muzzleVelocity")) - muzzleVelocity = databaseEntry[L"muzzleVelocity"].as_number().to_double(); - if (databaseEntry.has_number_field(L"aimTime")) - aimTime = databaseEntry[L"aimTime"].as_number().to_double(); - if (databaseEntry.has_number_field(L"shotsToFire")) - shotsToFire = databaseEntry[L"shotsToFire"].as_number().to_uint32(); - if (databaseEntry.has_number_field(L"engagementRange")) - engagementRange = databaseEntry[L"engagementRange"].as_number().to_double(); - if (databaseEntry.has_number_field(L"shotsBaseInterval")) - shotsBaseInterval = databaseEntry[L"shotsBaseInterval"].as_number().to_double(); - if (databaseEntry.has_number_field(L"shotsBaseScatter")) - shotsBaseScatter = databaseEntry[L"shotsBaseScatter"].as_number().to_double(); - if (databaseEntry.has_number_field(L"targetingRange")) - targetingRange = databaseEntry[L"targetingRange"].as_number().to_double(); - if (databaseEntry.has_number_field(L"aimMethodRange")) - aimMethodRange = databaseEntry[L"aimMethodRange"].as_number().to_double(); - if (databaseEntry.has_number_field(L"acquisitionRange")) - acquisitionRange = databaseEntry[L"acquisitionRange"].as_number().to_double(); - } - /* Get all the units in range and select one at random */ double range = max(max(engagementRange, aimMethodRange), acquisitionRange); map targets = unitsManager->getUnitsInRange(this, targetCoalition, { "Aircraft", "Helicopter" }, range); @@ -392,12 +405,17 @@ void GroundUnit::AIloop() /* Only do if we have a valid target close enough for AAA */ if (target != nullptr) { + taskString += "Missing on purpose. Valid target at range: " + to_string((int) round(distance)) + "m"; + + double correctedAimTime = aimTime; /* Approximate the flight time */ if (muzzleVelocity != 0) - aimTime += distance / muzzleVelocity; + correctedAimTime += distance / muzzleVelocity; /* If the target is in targeting range and we are in highest precision mode, target it */ if (distance < targetingRange && shotsScatter == ShotsScatter::LOW) { + taskString += ". Range is less than targeting range (" + to_string((int) round(targetingRange)) + "m) and scatter is LOW, aiming at target."; + /* Send the command */ std::ostringstream taskSS; taskSS.precision(10); @@ -406,37 +424,47 @@ void GroundUnit::AIloop() scheduler->appendCommand(command); setHasTask(true); - internalCounter = static_cast((aimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); + internalCounter = static_cast((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); } /* Else, do miss on purpose */ else { /* Compute where the target will be in aimTime seconds, plus the effect of scatter. */ double scatterDistance = distance * tan(shotsBaseScatter * (ShotsScatter::LOW - shotsScatter) / 57.29577) * (RANDOM_ZERO_TO_ONE - 0.1); - double aimDistance = target->getHorizontalVelocity() * aimTime + scatterDistance; + double aimDistance = target->getHorizontalVelocity() * correctedAimTime + scatterDistance; double aimLat = 0; double aimLng = 0; Geodesic::WGS84().Direct(target->getPosition().lat, target->getPosition().lng, target->getTrack() * 57.29577, aimDistance, aimLat, aimLng); /* TODO make util to convert degrees and radians function */ - double aimAlt = target->getPosition().alt + target->getVerticalVelocity() * aimTime + distance * tan(shotsBaseScatter * (ShotsScatter::LOW - shotsScatter) / 57.29577) * RANDOM_ZERO_TO_ONE; // Force to always miss high never low + double aimAlt = target->getPosition().alt + target->getVerticalVelocity() * correctedAimTime + distance * tan(shotsBaseScatter * (ShotsScatter::LOW - shotsScatter) / 57.29577) * RANDOM_ZERO_TO_ONE; // Force to always miss high never low /* Send the command */ if (distance < engagementRange) { + taskString += ". Range is less than engagement range (" + to_string((int) round(engagementRange)) + "m), using FIRE AT POINT method"; + /* If the unit is closer than the engagement range, use the fire at point method */ std::ostringstream taskSS; taskSS.precision(10); taskSS << "{id = 'FireAtPoint', lat = " << aimLat << ", lng = " << aimLng << ", alt = " << aimAlt << ", radius = 0.001, expendQty = " << shotsToFire << " }"; + + taskString += ". Aiming altitude " + to_string((int)round((aimAlt - position.alt) / 0.3048)) + "ft AGL"; Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); scheduler->appendCommand(command); setHasTask(true); setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); - internalCounter = static_cast((aimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); + internalCounter = static_cast((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); } else if (distance < aimMethodRange) { + taskString += ". Range is less than aim method range (" + to_string((int)round(aimMethodRange / 0.3048)) + "ft), using AIM method."; + /* If the unit is closer than the aim method range, use the aim method range */ - aimAtPoint(Coords(aimLat, aimLng, aimAlt)); + string aimMethodTask = aimAtPoint(Coords(aimLat, aimLng, aimAlt)); + taskString += aimMethodTask; + setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); - internalCounter = static_cast((aimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); + internalCounter = static_cast((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); } else { + taskString += ". Target is not in range of weapon, waking up unit to get ready for tasking."; + /* Else just wake the unit up with an impossible command */ std::ostringstream taskSS; taskSS.precision(10); @@ -453,6 +481,7 @@ void GroundUnit::AIloop() missOnPurposeTarget = target; } else { + taskString += "Missing on purpose. No target in range."; if (getHasTask()) resetTask(); } @@ -466,7 +495,7 @@ void GroundUnit::AIloop() if (databaseEntry.has_number_field(L"alertnessTimeConstant")) alertnessTimeConstant = databaseEntry[L"alertnessTimeConstant"].as_number().to_double(); } - internalCounter = static_cast((5 + RANDOM_ZERO_TO_ONE * alertnessTimeConstant * 0 /* TODO: remove to enable alertness again */) / FRAMERATE_TIME_INTERVAL); + internalCounter = static_cast((5 + RANDOM_ZERO_TO_ONE * alertnessTimeConstant) / FRAMERATE_TIME_INTERVAL); missOnPurposeTarget = nullptr; setTargetPosition(Coords(NULL)); } @@ -476,6 +505,11 @@ void GroundUnit::AIloop() setState(State::IDLE); } + setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL); + + if (taskString.length() > 0) + setTask(taskString); + break; } default: @@ -484,7 +518,8 @@ void GroundUnit::AIloop() } -void GroundUnit::aimAtPoint(Coords aimTarget) { +string GroundUnit::aimAtPoint(Coords aimTarget) { + string taskString = ""; double dist; double bearing1; double bearing2; @@ -521,7 +556,8 @@ void GroundUnit::aimAtPoint(Coords aimTarget) { double lng = 0; Geodesic::WGS84().Direct(position.lat, position.lng, bearing1, r, lat, lng); - log(unitName + "(" + name + ")" + " shooting with aim at point method. Barrel elevation: " + to_string(barrelElevation * 57.29577) + "°, bearing: " + to_string(bearing1) + "°"); + taskString = +"Barrel elevation: " + to_string((int) round(barrelElevation)) + "m, bearing: " + to_string((int) round(bearing1)) + "deg"; + log(unitName + "(" + name + ")" + " shooting with aim at point method. Barrel elevation: " + to_string(barrelElevation) + "m, bearing: " + to_string(bearing1) + "°"); std::ostringstream taskSS; taskSS.precision(10); @@ -532,7 +568,10 @@ void GroundUnit::aimAtPoint(Coords aimTarget) { } else { log("Target out of range for " + unitName + "(" + name + ")"); + taskString = +"Target out of range"; } + + return taskString; } void GroundUnit::changeSpeed(string change) diff --git a/backend/core/src/scheduler.cpp b/backend/core/src/scheduler.cpp index 5e167a53..30243911 100644 --- a/backend/core/src/scheduler.cpp +++ b/backend/core/src/scheduler.cpp @@ -509,6 +509,29 @@ void Scheduler::handleRequest(string key, json::value value, string username, js } } /************************/ + else if (key.compare("setEngagementProperties") == 0) + { + unsigned int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + { + /* Engagement properties tasking */ + unit->setBarrelHeight(value[L"barrelHeight"].as_number().to_double()); + unit->setMuzzleVelocity(value[L"muzzleVelocity"].as_number().to_double()); + unit->setAimTime(value[L"aimTime"].as_number().to_double()); + unit->setShotsToFire(value[L"shotsToFire"].as_number().to_uint32()); + unit->setShotsBaseInterval(value[L"shotsBaseInterval"].as_number().to_double()); + unit->setShotsBaseScatter(value[L"shotsBaseScatter"].as_number().to_double()); + unit->setEngagementRange(value[L"engagementRange"].as_number().to_double()); + unit->setTargetingRange(value[L"targetingRange"].as_number().to_double()); + unit->setAimMethodRange(value[L"aimMethodRange"].as_number().to_double()); + unit->setAcquisitionRange(value[L"acquisitionRange"].as_number().to_double()); + + log(username + " updated unit " + unit->getUnitName() + "(" + unit->getName() + ") engagementProperties", true); + } + } + /************************/ else if (key.compare("setFollowRoads") == 0) { unsigned int ID = value[L"ID"].as_integer(); diff --git a/backend/core/src/unit.cpp b/backend/core/src/unit.cpp index 480e5242..48e1383f 100644 --- a/backend/core/src/unit.cpp +++ b/backend/core/src/unit.cpp @@ -298,6 +298,17 @@ void Unit::getData(stringstream& ss, unsigned long long time) case DataIndex::racetrackLength: appendNumeric(ss, datumIndex, racetrackLength); break; case DataIndex::racetrackAnchor: appendNumeric(ss, datumIndex, racetrackAnchor); break; case DataIndex::racetrackBearing: appendNumeric(ss, datumIndex, racetrackBearing); break; + case DataIndex::timeToNextTasking: appendNumeric(ss, datumIndex, timeToNextTasking); break; + case DataIndex::barrelHeight: appendNumeric(ss, datumIndex, barrelHeight); break; + case DataIndex::muzzleVelocity: appendNumeric(ss, datumIndex, muzzleVelocity); break; + case DataIndex::aimTime: appendNumeric(ss, datumIndex, aimTime); break; + case DataIndex::shotsToFire: appendNumeric(ss, datumIndex, shotsToFire); break; + case DataIndex::shotsBaseInterval: appendNumeric(ss, datumIndex, shotsBaseInterval); break; + case DataIndex::shotsBaseScatter: appendNumeric(ss, datumIndex, shotsBaseScatter); break; + case DataIndex::engagementRange: appendNumeric(ss, datumIndex, engagementRange); break; + case DataIndex::targetingRange: appendNumeric(ss, datumIndex, targetingRange); break; + case DataIndex::aimMethodRange: appendNumeric(ss, datumIndex, aimMethodRange); break; + case DataIndex::acquisitionRange: appendNumeric(ss, datumIndex, acquisitionRange); break; } } } diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index e9b8cbde..2638cb9b 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -385,7 +385,7 @@ export enum WarningSubstate { NO_SUBSTATE = "No substate", NOT_CHROME = "Not chrome", NOT_SECURE = "Not secure", - ERROR_UPLOADING_CONFIG = "Error uploading config" + ERROR_UPLOADING_CONFIG = "Error uploading config", } export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | OptionsSubstate | string; @@ -418,7 +418,7 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = { AWACSCoalition: "blue", hideChromeWarning: false, hideSecureWarning: false, - showMissionDrawings: false + showMissionDrawings: false, }; export const MAP_HIDDEN_TYPES_DEFAULTS = { @@ -497,6 +497,17 @@ export enum DataIndexes { racetrackLength, racetrackAnchor, racetrackBearing, + timeToNextTasking, + barrelHeight, + muzzleVelocity, + aimTime, + shotsToFire, + shotsBaseInterval, + shotsBaseScatter, + engagementRange, + targetingRange, + aimMethodRange, + acquisitionRange, endOfData = 255, } @@ -530,7 +541,6 @@ export enum ContextActionType { DELETE, } - export enum colors { ALICE_BLUE = "#F0F8FF", ANTIQUE_WHITE = "#FAEBD7", @@ -679,7 +689,7 @@ export enum colors { OLYMPUS_BLUE = "#243141", OLYMPUS_RED = "#F05252", OLYMPUS_ORANGE = "#FF9900", - OLYMPUS_GREEN = "#8BFF63" + OLYMPUS_GREEN = "#8BFF63", } export const CONTEXT_ACTION_COLORS = [undefined, colors.WHITE, colors.GREEN, colors.PURPLE, colors.BLUE, colors.RED]; @@ -919,7 +929,9 @@ export namespace ContextActions { ContextActionTarget.POINT, (units: Unit[], _, targetPosition: LatLng | null) => { if (targetPosition) - getApp().getUnitsManager().fireInfrared(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units); + getApp() + .getUnitsManager() + .fireInfrared(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units); }, { type: ContextActionType.ENGAGE, code: "KeyL", ctrlKey: true, shiftKey: false } ); diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index ee453d7e..f99e9f2f 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -40,7 +40,8 @@ export interface OlympusConfig { /* Set by server */ local?: boolean; - authentication?: { // Only sent when in localhost mode for autologin + authentication?: { + // Only sent when in localhost mode for autologin gameMasterPassword: string; blueCommanderPasword: string; redCommanderPassword: string; @@ -53,12 +54,12 @@ export interface SessionData { unitSinks?: { ID: number }[]; connections?: any[]; coalitionAreas?: ( - | { type: 'circle', label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition } - | { type: 'polygon', label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition } + | { type: "circle"; label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition } + | { type: "polygon"; label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition } )[]; - hotgroups?: {[key: string]: number[]}, - starredSpawns?: { [key: number]: SpawnRequestTable } - drawings?: { [key: string]: {visibility: boolean, opacity: number, name: string, guid: string, containers: any, drawings: any} } + hotgroups?: { [key: string]: number[] }; + starredSpawns?: { [key: number]: SpawnRequestTable }; + drawings?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } }; } export interface ProfileOptions { @@ -88,7 +89,7 @@ export interface BullseyesData { export interface SpotsData { spots: { - [key: string]: { type: string, targetPosition: {lat: number; lng: number}; sourceUnitID: number; code?: number }; + [key: string]: { type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number }; }; sessionHash: string; time: number; @@ -265,6 +266,17 @@ export interface UnitData { racetrackLength: number; racetrackAnchor: LatLng; racetrackBearing: number; + timeToNextTasking: number; + barrelHeight: number; + muzzleVelocity: number; + aimTime: number; + shotsToFire: number; + shotsBaseInterval: number; + shotsBaseScatter: number; + engagementRange: number; + targetingRange: number; + aimMethodRange: number; + acquisitionRange: number; } export interface LoadoutItemBlueprint { @@ -400,4 +412,4 @@ export interface Drawing { hiddenOnPlanner?: boolean; file?: string; scale?: number; -} \ No newline at end of file +} diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index f80ce8ee..0f5307f9 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -554,6 +554,38 @@ export class ServerManager { this.PUT(data, callback); } + setEngagementProperties( + ID: number, + barrelHeight: number, + muzzleVelocity: number, + aimTime: number, + shotsToFire: number, + shotsBaseInterval: number, + shotsBaseScatter: number, + engagementRange: number, + targetingRange: number, + aimMethodRange: number, + acquisitionRange: number, + callback: CallableFunction = () => {} + ) { + var command = { + ID: ID, + barrelHeight: barrelHeight, + muzzleVelocity: muzzleVelocity, + aimTime: aimTime, + shotsToFire: shotsToFire, + shotsBaseInterval: shotsBaseInterval, + shotsBaseScatter: shotsBaseScatter, + engagementRange: engagementRange, + targetingRange: targetingRange, + aimMethodRange: aimMethodRange, + acquisitionRange: acquisitionRange, + } + + var data = { setEngagementProperties: command }; + this.PUT(data, callback); + } + setCommandModeOptions(commandModeOptions: CommandModeOptions, callback: CallableFunction = () => {}) { var data = { setCommandModeOptions: commandModeOptions }; this.PUT(data, callback); diff --git a/frontend/react/src/ui/components/olnumberinput.tsx b/frontend/react/src/ui/components/olnumberinput.tsx index ce2f352a..15ff7cbb 100644 --- a/frontend/react/src/ui/components/olnumberinput.tsx +++ b/frontend/react/src/ui/components/olnumberinput.tsx @@ -11,6 +11,7 @@ export function OlNumberInput(props: { tooltip?: string | (() => JSX.Element | JSX.Element[]); tooltipPosition?: string; tooltipRelativeToParent?: boolean; + decimalPlaces?: number; onDecrease: () => void; onIncrease: () => void; onChange: (e: ChangeEvent) => void; @@ -89,7 +90,7 @@ export function OlNumberInput(props: { dark:focus:ring-blue-700 focus:border-blue-700 focus:ring-blue-500 `} - value={zeroAppend(props.value, props.minLength ?? 0)} + value={zeroAppend(props.value, props.minLength ?? 0, props.decimalPlaces !== undefined, props.decimalPlaces ?? 0)} /> + + )} )} @@ -1825,14 +2333,18 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { -
- {selectedUnits[0].getTask()} -
+
{selectedUnits[0].getTask()}
+ {([UnitState.SIMULATE_FIRE_FIGHT, UnitState.MISS_ON_PURPOSE, UnitState.SCENIC_AAA] as string[]).includes(selectedUnits[0].getState()) && ( +
+ Time to next tasking: {zeroAppend(selectedUnits[0].getTimeToNextTasking(), 0, true, 2)}s +
+ )} +
- -
{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft
+ +
{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft
diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 4f61d188..8be5071b 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -177,6 +177,17 @@ export abstract class Unit extends CustomMarker { #spotLines: { [key: number]: Polyline } = {}; #spotEditMarkers: { [key: number]: SpotEditMarker } = {}; #spotMarkers: { [key: number]: SpotMarker } = {}; + #timeToNextTasking: number = 0; + #barrelHeight: number = 0; + #muzzleVelocity: number = 0; + #aimTime: number = 0; + #shotsToFire: number = 0; + #shotsBaseInterval: number = 0; + #shotsBaseScatter: number = 0; + #engagementRange: number = 0; + #targetingRange: number = 0; + #aimMethodRange: number = 0; + #acquisitionRange: number = 0; /* Inputs timers */ #debounceTimeout: number | null = null; @@ -332,6 +343,39 @@ export abstract class Unit extends CustomMarker { getRaceTrackBearing() { return this.#racetrackBearing; } + getTimeToNextTasking() { + return this.#timeToNextTasking; + } + getBarrelHeight() { + return this.#barrelHeight; + } + getMuzzleVelocity() { + return this.#muzzleVelocity; + } + getAimTime() { + return this.#aimTime; + } + getShotsToFire() { + return this.#shotsToFire; + } + getShotsBaseInterval() { + return this.#shotsBaseInterval; + } + getShotsBaseScatter() { + return this.#shotsBaseScatter; + } + getEngagementRange() { + return this.#engagementRange; + } + getTargetingRange() { + return this.#targetingRange; + } + getAimMethodRange() { + return this.#aimMethodRange; + } + getAcquisitionRange() { + return this.#acquisitionRange; + } static getConstructor(type: string) { if (type === "GroundUnit") return GroundUnit; @@ -644,6 +688,41 @@ export abstract class Unit extends CustomMarker { case DataIndexes.racetrackBearing: this.#racetrackBearing = dataExtractor.extractFloat64(); break; + case DataIndexes.timeToNextTasking: + this.#timeToNextTasking = dataExtractor.extractFloat64(); + break; + case DataIndexes.barrelHeight: + this.#barrelHeight = dataExtractor.extractFloat64(); + break; + case DataIndexes.muzzleVelocity: + this.#muzzleVelocity = dataExtractor.extractFloat64(); + break; + case DataIndexes.aimTime: + this.#aimTime = dataExtractor.extractFloat64(); + break; + case DataIndexes.shotsToFire: + this.#shotsToFire = dataExtractor.extractUInt32(); + break; + case DataIndexes.shotsBaseInterval: + this.#shotsBaseInterval = dataExtractor.extractFloat64(); + break; + case DataIndexes.shotsBaseScatter: + this.#shotsBaseScatter = dataExtractor.extractFloat64(); + break; + case DataIndexes.engagementRange: + this.#engagementRange = dataExtractor.extractFloat64(); + break; + case DataIndexes.targetingRange: + this.#targetingRange = dataExtractor.extractFloat64(); + break; + case DataIndexes.aimMethodRange: + this.#aimMethodRange = dataExtractor.extractFloat64(); + break; + case DataIndexes.acquisitionRange: + this.#acquisitionRange = dataExtractor.extractFloat64(); + break; + default: + break; } } @@ -750,6 +829,17 @@ export abstract class Unit extends CustomMarker { racetrackLength: this.#racetrackLength, racetrackAnchor: this.#racetrackAnchor, racetrackBearing: this.#racetrackBearing, + timeToNextTasking: this.#timeToNextTasking, + barrelHeight: this.#barrelHeight, + muzzleVelocity: this.#muzzleVelocity, + aimTime: this.#aimTime, + shotsToFire: this.#shotsToFire, + shotsBaseInterval: this.#shotsBaseInterval, + shotsBaseScatter: this.#shotsBaseScatter, + engagementRange: this.#engagementRange, + targetingRange: this.#targetingRange, + aimMethodRange: this.#aimMethodRange, + acquisitionRange: this.#acquisitionRange, }; } @@ -1307,6 +1397,36 @@ export abstract class Unit extends CustomMarker { if (!this.#human) getApp().getServerManager().setAdvancedOptions(this.ID, isActiveTanker, isActiveAWACS, TACAN, radio, generalSettings); } + setEngagementProperties( + barrelHeight: number, + muzzleVelocity: number, + aimTime: number, + shotsToFire: number, + shotsBaseInterval: number, + shotsBaseScatter: number, + engagementRange: number, + targetingRange: number, + aimMethodRange: number, + acquisitionRange: number + ) { + if (!this.#human) + getApp() + .getServerManager() + .setEngagementProperties( + this.ID, + barrelHeight, + muzzleVelocity, + aimTime, + shotsToFire, + shotsBaseInterval, + shotsBaseScatter, + engagementRange, + targetingRange, + aimMethodRange, + acquisitionRange + ); + } + bombPoint(latlng: LatLng) { getApp().getServerManager().bombPoint(this.ID, latlng); } diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index 67e662f2..39003f66 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -546,6 +546,7 @@ export class UnitsManager { units = units.filter((unit) => !unit.getHuman()); let callback = (units: Unit[]) => { + onExecution(); for (let idx in units) { getApp().getServerManager().addDestination(units[idx].ID, []); } @@ -819,16 +820,24 @@ export class UnitsManager { } /** Set the advanced options for the selected units - * + * * @param isActiveTanker If true, the unit will be a tanker * @param isActiveAWACS If true, the unit will be an AWACS * @param TACAN TACAN settings * @param radio Radio settings * @param generalSettings General settings * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. - * @param onExecution Function to execute after the operation is completed - */ - setAdvancedOptions(isActiveTanker: boolean, isActiveAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings, units: Unit[] | null = null, onExecution: () => void = () => {}) { + * @param onExecution Function to execute after the operation is completed + */ + setAdvancedOptions( + isActiveTanker: boolean, + isActiveAWACS: boolean, + TACAN: TACAN, + radio: Radio, + generalSettings: GeneralSettings, + units: Unit[] | null = null, + onExecution: () => void = () => {} + ) { if (units === null) units = this.getSelectedUnits(); units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { @@ -836,7 +845,49 @@ export class UnitsManager { units.forEach((unit: Unit) => unit.setAdvancedOptions(isActiveTanker, isActiveAWACS, TACAN, radio, generalSettings)); this.#showActionMessage(units, `advanced options set`); }; - + + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) { + getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION); + this.#protectionCallback = callback; + } else callback(units); + } + + //TODO + setEngagementProperties( + barrelHeight: number, + muzzleVelocity: number, + aimTime: number, + shotsToFire: number, + shotsBaseInterval: number, + shotsBaseScatter: number, + engagementRange: number, + targetingRange: number, + aimMethodRange: number, + acquisitionRange: number, + units: Unit[] | null = null, + onExecution: () => void = () => {} + ) { + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => !unit.getHuman()); + let callback = (units) => { + onExecution(); + units.forEach((unit: Unit) => + unit.setEngagementProperties( + barrelHeight, + muzzleVelocity, + aimTime, + shotsToFire, + shotsBaseInterval, + shotsBaseScatter, + engagementRange, + targetingRange, + aimMethodRange, + acquisitionRange + ) + ); + this.#showActionMessage(units, `engagement parameters set`); + }; + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION); this.#protectionCallback = callback; diff --git a/manager/javascripts/filesystem.js b/manager/javascripts/filesystem.js index 6bea517b..9b6b0634 100644 --- a/manager/javascripts/filesystem.js +++ b/manager/javascripts/filesystem.js @@ -66,8 +66,8 @@ async function installMod(folder, name) { logger.log(`Mod succesfully installed in ${folder}`) /* Check if backup user-editable files exist. If true copy them over */ - logger.log(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases")); - if (await exists(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"))) { + logger.log(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases", "units", "mods.json")); + if (await exists(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases", "units", "mods.json"))) { logger.log("Backup databases found, copying over"); // Changed in v2.0.0, only the mods database is copied over, if present diff --git a/notes.txt b/notes.txt index f278b2f1..123e1f22 100644 --- a/notes.txt +++ b/notes.txt @@ -1,3 +1,11 @@ +v2.0.0 ==================== +Changes: +1) completely redone UI using React +2) added audio backend for SRS integration +3) added Mission Editor drawings in DCS +4) multiple enhancements to control scheme + + v1.0.4 ==================== Changes: 1) Added Olympus Manager for unified configuration management; diff --git a/olympus.json b/olympus.json index e8d24e36..7fd166ea 100644 --- a/olympus.json +++ b/olympus.json @@ -1,53 +1,53 @@ { + "_comment1": "These are the address and port of the backend server, i.e. the server run by the Olympus dll mod.", + "_comment2": "localhost should be used if the backend is running on the same machine as the frontend server, which is usually the case.", + "_comment3": "If a direct connection is desired, e.g. for API usage, use '*' as address.", + "_comment4": "The desired port should be available and not used by other processes.", "backend": { - "_comment1": "These are the address and port of the backend server, i.e. the server run by the Olympus dll mod.", - "_comment2": "localhost should be used if the backend is running on the same machine as the frontend server, which is usually the case.", - "_comment3": "If a direct connection is desired, e.g. for API usage, use '*' as address.", - "_comment4": "The desired port should be available and not used by other processes.", - "address": "localhost", "port": 4512 }, - "authentication": { - "_comment1": "These are the sha256 hashed passwords for the game master, the two commanders, and the admin. They are used to authenticate the users.", + "_comment5": "These are the sha256 hashed passwords for the game master, the two commanders, and the admin. They are used to authenticate the users.", + "authentication": { "gameMasterPassword": "", "blueCommanderPassword": "", "redCommanderPassword": "", "admin": "" }, - "frontend": { - "_comment1": "These are the settings for the frontend server, i.e. the server which hosts the Olympus GUI web interface.", - "_comment2": "The port should be available and not used by other processes and is used to load the interface.", + "_comment6": "These are the settings for the frontend server, i.e. the server which hosts the Olympus GUI web interface.", + "_comment7": "The port should be available and not used by other processes and is used to load the interface.", + "frontend": { "port": 3000, + + "_comment8": "These are the custom headers used for authentication. They are used to authenticate the users and skip the login page", + "_comment9": "If enabled, the frontend server will look for the specified headers in the request and use them to authenticate the user.", + "_comment10": "The username header should contain the username and the group header should contain the group of the user.", + "_comment11": "If the headers are not present or the user is not authenticated, the user will be redirected to the login page.", + "_comment12": "This is useful for integrating Olympus with other systems, e.g. a SSO provider", + "_comment13": "The group should be one of the groups defined using the admin page on the web interface", + "_comment14": "If the user is by default authorized to more than one command mode, x-command-mode header can be used to specify the default command mode", + "_comment15": "Otherwise, the login page will be skipped, but a command more selection page will still be shown", "customAuthHeaders": { - "_comment1": "These are the custom headers used for authentication. They are used to authenticate the users and skip the login page", - "_comment2": "If enabled, the frontend server will look for the specified headers in the request and use them to authenticate the user.", - "_comment3": "The username header should contain the username and the group header should contain the group of the user.", - "_comment4": "If the headers are not present or the user is not authenticated, the user will be redirected to the login page.", - "_comment5": "This is useful for integrating Olympus with other systems, e.g. a SSO provider", - "_comment6": "The group should be one of the groups defined using the admin page on the web interface", - "_comment7": "If the user is by default authorized to more than one command mode, x-command-mode header can be used to specify the default command mode", - "_comment8": "Otherwise, the login page will be skipped, but a command more selection page will still be shown", - "enabled": false, "username": "X-Authorized", "group": "X-Group" }, - "elevationProvider": { - "_comment1": "The elevation provider is used to fetch elevation data for the map. It should be a URL with {lat} and {lng} placeholders.", + "_comment16": "The elevation provider is used to fetch elevation data for the map. It should be a URL with {lat} and {lng} placeholders.", + "elevationProvider": { "provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip", "username": null, "password": null }, - "mapLayers": { - "_comment1": "These are the map layers used by the frontend server. They are used to display the map in the interface.", - "_comment2": "The urlTemplate should be a URL with {z}, {x}, and {y} placeholders for the zoom level and tile coordinates.", - "_comment3": "The minZoom and maxZoom define the zoom levels at which the layer is visible.", - "_comment4": "The attribution is the text displayed in the bottom right corner of the map.", + + "_comment17": "These are the map layers used by the frontend server. They are used to display the map in the interface.", + "_comment18": "The urlTemplate should be a URL with {z}, {x}, and {y} placeholders for the zoom level and tile coordinates.", + "_comment19": "The minZoom and maxZoom define the zoom levels at which the layer is visible.", + "_comment20": "The attribution is the text displayed in the bottom right corner of the map.", + "mapLayers": { "ArcGIS Satellite": { "urlTemplate": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", "minZoom": 1, @@ -61,10 +61,10 @@ "attribution": "OpenStreetMap contributors" } }, - "mapMirrors": { - "_comment1": "These are the map mirrors used by the frontend server. They are used to load the map tiles from different sources.", - "_comment2": "The key is the name of the mirror and the value is the URL of the mirror.", + "_comment21": "These are the map mirrors used by the frontend server. They are used to load the map tiles from different sources.", + "_comment22": "The key is the name of the mirror and the value is the URL of the mirror.", + "mapMirrors": { "DCS Map (Official)": "https://maps.dcsolympus.com/maps", "DCS Map (Alt.)": "https://refugees.dcsolympus.com/maps" }, @@ -72,16 +72,17 @@ "_comment3": "If autoconnectWhenLocal is true, the frontend server will automatically connect to the backend server when running on the same machine.", "_comment4": "If a proxy is used, the proxyHeader should be set to the header used to forward the client IP address to the backend server.", "_comment5": "This is useful when running the frontend server behind a reverse proxy, e.g. nginx, allowing to skip login when connecting locally but still authenticate when connecting remotely.", - "autoconnectWhenLocal": true, "proxyHeader": "x-forwarded-for" }, - "audio": { - "_comment1": "These are the settings for the audio backend, i.e. the service which handles direct connection of Olympus to a SRS server.", - "_comment2": "The SRSPort is the port used to connect to the SRS server and should be set to be the same as the value in SRS (5002 by default).", - "_comment3": "The WSPort is the port used by the web interface to connect to the audio backend WebSocket. It should be available and not used by other processes.", - "_comment4": "The WSEndpoint is the endpoint used by the web interface to connect to the audio backend WebSocket when using a reverse proxy. A websocket proxy should be set up to forward requests from this endpoint to WSPort.", + "_comment23": "These are the settings for the audio backend, i.e. the service which handles direct connection of Olympus to a SRS server.", + "_comment24": "The SRSPort is the port used to connect to the SRS server and should be set to be the same as the value in SRS (5002 by default).", + "_comment25": "The WSPort is the port used by the web interface to connect to the audio backend WebSocket. It should be available and not used by other processes.", + "_comment26": "The WSEndpoint is the endpoint used by the web interface to connect to the audio backend WebSocket when using a reverse proxy. A websocket proxy should be set up to forward requests from this endpoint to WSPort.", + + "audio": { + "SRSPort": 5002, "WSPort": 4000, "WSEndpoint": "audio" From 760fe18cc77092c532bbdd0b904d4848ded1779d Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 13 Mar 2025 09:39:34 +0100 Subject: [PATCH 12/33] fix: Autoconnect when local checkbox not reflecting configuration --- manager/javascripts/dcsinstance.js | 2 +- manager/javascripts/manager.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/manager/javascripts/dcsinstance.js b/manager/javascripts/dcsinstance.js index 9aa93fd1..ca9ca63c 100644 --- a/manager/javascripts/dcsinstance.js +++ b/manager/javascripts/dcsinstance.js @@ -190,7 +190,7 @@ class DCSInstance { this.gameMasterPasswordHash = config["authentication"]["gameMasterPassword"]; /* Read the new configurations added in v2.0.0 */ - if ( config["frontend"]["autoconnectWhenLocal"] !== undefined) + if (config["frontend"]["autoconnectWhenLocal"] !== undefined) this.autoconnectWhenLocal = config["frontend"]["autoconnectWhenLocal"]; if (config["frontend"]["audio"] !== undefined && config["frontend"]["audio"]["SRSPort"] !== undefined) this.SRSPort = config["audio"]["SRSPort"]; diff --git a/manager/javascripts/manager.js b/manager/javascripts/manager.js index 3bcbedb3..691bedc8 100644 --- a/manager/javascripts/manager.js +++ b/manager/javascripts/manager.js @@ -159,6 +159,7 @@ class Manager { if (this.getActiveInstance()) { this.setPort('frontend', this.getActiveInstance().frontendPort); this.setPort('backend', this.getActiveInstance().backendPort); + this.expertSettingsPage.getElement().querySelector(".autoconnect .checkbox").classList.toggle("checked", this.getActiveInstance().autoconnectWhenLocal) } } From 5acc0e8ac567f302b08c89d40bea67438c1cb38a Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 13 Mar 2025 16:54:16 +0100 Subject: [PATCH 13/33] fix: Audio backend working if both port and endpoint added Added ability to use uri for backend address (remote debugging) --- frontend/react/src/audio/audiomanager.ts | 24 ++++++++----- frontend/server/src/app.ts | 43 ++++++++++++++-------- frontend/server/src/routes/admin.ts | 45 ++++++++++++++++-------- 3 files changed, 75 insertions(+), 37 deletions(-) diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 8c4e0094..be88f287 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -54,10 +54,10 @@ export class AudioManager { constructor() { ConfigLoadedEvent.on((config: OlympusConfig) => { - if (config.audio) - config.audio.WSPort ? this.setPort(config.audio.WSPort) : this.setEndpoint(config.audio.WSEndpoint); - else - console.error("No audio configuration found in the Olympus configuration file"); + if (config.audio) { + this.setPort(config.audio.WSPort); + this.setEndpoint(config.audio.WSEndpoint); + } else console.error("No audio configuration found in the Olympus configuration file"); }); CommandModeOptionsChangedEvent.on((options: CommandModeOptions) => { @@ -101,11 +101,19 @@ export class AudioManager { let wsAddress = res ? res[1] : location.toString(); if (wsAddress.at(wsAddress.length - 1) === "/") wsAddress = wsAddress.substring(0, wsAddress.length - 1); - if (this.#endpoint) this.#socket = new WebSocket(`wss://${wsAddress}/${this.#endpoint}`); - 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"); - if (!this.#socket) return; + if (this.#port === undefined && this.#endpoint === undefined) { + console.error("The audio backend was enabled but no port/endpoint was provided in the configuration"); + return; + } + + this.#socket = new WebSocket(`wss://${wsAddress}/${this.#endpoint}`); + if (!this.#socket) this.#socket = new WebSocket(`ws://${wsAddress}:${this.#port}`); + + if (!this.#socket) { + console.error("Failed to connect to audio websocket"); + return; + } /* Log the opening of the connection */ this.#socket.addEventListener("open", (event) => { diff --git a/frontend/server/src/app.ts b/frontend/server/src/app.ts index b74b57e9..0ceb1a37 100644 --- a/frontend/server/src/app.ts +++ b/frontend/server/src/app.ts @@ -126,9 +126,13 @@ module.exports = function (configLocation, viteProxy) { }); /* Define middleware */ - app.use(logger('dev', { - skip: function (req, res) { return res.statusCode < 400 } - })); + app.use( + logger("dev", { + skip: function (req, res) { + return res.statusCode < 400; + }, + }) + ); /* Authorization middleware */ if ( @@ -166,7 +170,7 @@ module.exports = function (configLocation, viteProxy) { app.use("/olympus", async (req, res, next) => { /* Check if custom authorization headers are being used */ const user = - //@ts-ignore + //@ts-ignore req.auth?.user ?? checkCustomHeaders(config, usersConfig, groupsConfig, req); @@ -221,16 +225,27 @@ module.exports = function (configLocation, viteProxy) { }); /* Proxy middleware */ - app.use( - "/olympus", - httpProxyMiddleware.createProxyMiddleware({ - target: `http://${ - backendAddress === "*" ? "localhost" : backendAddress - }:${config["backend"]["port"]}/olympus`, - changeOrigin: true, - }) - ); - + if (config["backend"]["port"]) { + app.use( + "/olympus", + httpProxyMiddleware.createProxyMiddleware({ + target: `http://${ + backendAddress === "*" ? "localhost" : backendAddress + }:${config["backend"]["port"]}/olympus`, + changeOrigin: true, + }) + ); + } else { + app.use( + "/olympus", + httpProxyMiddleware.createProxyMiddleware({ + target: `https://${ + backendAddress === "*" ? "localhost" : backendAddress + }/olympus`, + changeOrigin: true, + }) + ); + } app.use(bodyParser.json({ limit: "50mb" })); app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); diff --git a/frontend/server/src/routes/admin.ts b/frontend/server/src/routes/admin.ts index d39a6a99..caed34ff 100644 --- a/frontend/server/src/routes/admin.ts +++ b/frontend/server/src/routes/admin.ts @@ -45,29 +45,44 @@ module.exports = function (configLocation) { router.put("/config", function (req, res, next) { if (req.auth?.user === "Admin") { /* Create a backup folder for the configuration files */ - let backupFolder = path.join(path.dirname(configLocation), "Olympus Configs Backup"); + let backupFolder = path.join( + path.dirname(configLocation), + "Olympus Configs Backup" + ); if (!fs.existsSync(backupFolder)) { fs.mkdirSync(backupFolder); } /* Make a backup of the existing files */ let timestamp = new Date().toISOString().replace(/:/g, "-"); - fs.copyFileSync( - path.join(path.dirname(configLocation), "olympusUsers.json"), - path.join( - path.dirname(configLocation), - "Olympus Configs Backup", - "olympusUsers.json." + timestamp + if ( + fs.existsSync( + path.join(path.dirname(configLocation), "olympusUsers.json") ) - ); - fs.copyFileSync( - path.join(path.dirname(configLocation), "olympusGroups.json"), - path.join( - path.dirname(configLocation), - "Olympus Configs Backup", - "olympusGroups.json." + timestamp + ) { + fs.copyFileSync( + path.join(path.dirname(configLocation), "olympusUsers.json"), + path.join( + path.dirname(configLocation), + "Olympus Configs Backup", + "olympusUsers.json." + timestamp + ) + ); + } + if ( + fs.existsSync( + path.join(path.dirname(configLocation), "olympusGroups.json") ) - ); + ) { + fs.copyFileSync( + path.join(path.dirname(configLocation), "olympusGroups.json"), + path.join( + path.dirname(configLocation), + "Olympus Configs Backup", + "olympusGroups.json." + timestamp + ) + ); + } /* Save the users configuration file */ let usersConfig = req.body.users; From f77b008701bdb9e283a83ca93548cbc469b82be5 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Thu, 13 Mar 2025 20:50:51 +0100 Subject: [PATCH 14/33] feat: Updated database for flak --- databases/units/groundunitdatabase.json | 38 +++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/databases/units/groundunitdatabase.json b/databases/units/groundunitdatabase.json index 43537996..186d2f54 100644 --- a/databases/units/groundunitdatabase.json +++ b/databases/units/groundunitdatabase.json @@ -8619,24 +8619,25 @@ "countries": "All" } }, - "acquisitionRange": 16000, - "engagementRange": 2000, + "acquisitionRange": 30000, + "engagementRange": 21000, "description": "KS-19. 100 mm AAA gun. Fixed manually aimed large calibre anti aircraft gun.", "abilities": "AA", "canTargetPoint": false, "canRearm": false, "muzzleVelocity": 1000, - "aimTime": 25, - "shotsToFire": 5, + "aimTime": 5, + "shotsToFire": 1, "barrelHeight": 5, "cost": null, "markerFile": "groundunit-aaa", "canAAA": true, - "targetingRange": 100, - "aimMethodRange": 15000, + "targetingRange": 0, + "aimMethodRange": 0, "shotsBaseInterval": 5, - "shotsBaseScatter": 5, - "alertnessTimeConstant": 5 + "shotsBaseScatter": 8, + "alertnessTimeConstant": 5, + "flak": true }, "SON_9": { "name": "SON_9", @@ -10651,7 +10652,8 @@ "shotsBaseScatter": 5, "aimMethodRange": 15000, "targetingRange": 200, - "alertnessTimeConstant": 15 + "alertnessTimeConstant": 15, + "flak": true }, "Pz_IV_H": { "name": "Pz_IV_H", @@ -11896,7 +11898,8 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": true, + "flak": true }, "Maschinensatz_33": { "name": "Maschinensatz_33", @@ -13069,7 +13072,8 @@ "name": "Winter", "countries": "All" } - } + }, + "flak": true }, "flak36": { "name": "flak36", @@ -13097,7 +13101,8 @@ "name": "Winter", "countries": "All" } - } + }, + "flak": true }, "flak37": { "name": "flak37", @@ -13125,7 +13130,8 @@ "name": "Winter", "countries": "All" } - } + }, + "flak": true }, "flak38": { "name": "flak38", @@ -13153,7 +13159,8 @@ "name": "Winter", "countries": "All" } - } + }, + "flak": true }, "flak41": { "name": "flak41", @@ -13181,7 +13188,8 @@ "name": "Winter", "countries": "All" } - } + }, + "flak": true }, "HEMTT_C-RAM_Phalanx": { "name": "HEMTT_C-RAM_Phalanx", From f0826bbdba3d0ecafa5de03c65e9eab207452a57 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Fri, 14 Mar 2025 16:45:46 +0100 Subject: [PATCH 15/33] fix: Fixed multiple errors in scenic AAA modes --- backend/core/include/unit.h | 27 +-- backend/core/include/weapon.h | 6 + backend/core/src/groundunit.cpp | 91 +++++----- backend/core/src/unit.cpp | 8 + backend/core/src/weapon.cpp | 7 + backend/core/src/weaponsmanager.cpp | 2 + databases/units/groundunitdatabase.json | 163 ++++++++++-------- .../images/units/map/awacs/blue/shell.svg | 43 +++++ .../images/units/map/awacs/neutral/shell.svg | 43 +++++ .../images/units/map/normal/blue/shell.svg | 43 +++++ .../images/units/map/normal/neutral/shell.svg | 43 +++++ .../images/units/map/normal/red/shell.svg | 43 +++++ frontend/react/src/map/map.ts | 32 ---- frontend/react/src/unit/unit.ts | 4 + frontend/react/src/weapon/weapon.ts | 38 ++++ frontend/react/src/weapon/weaponsmanager.ts | 2 +- scripts/lua/backend/OlympusCommand.lua | 2 + 17 files changed, 437 insertions(+), 160 deletions(-) create mode 100644 frontend/react/public/images/units/map/awacs/blue/shell.svg create mode 100644 frontend/react/public/images/units/map/awacs/neutral/shell.svg create mode 100644 frontend/react/public/images/units/map/normal/blue/shell.svg create mode 100644 frontend/react/public/images/units/map/normal/neutral/shell.svg create mode 100644 frontend/react/public/images/units/map/normal/red/shell.svg diff --git a/backend/core/include/unit.h b/backend/core/include/unit.h index ce2f1b83..a8a972c4 100644 --- a/backend/core/include/unit.h +++ b/backend/core/include/unit.h @@ -62,6 +62,8 @@ public: bool hasFreshData(unsigned long long time); bool checkFreshness(unsigned char datumIndex, unsigned long long time); + unsigned int computeTotalAmmo(); + /********** Setters **********/ virtual void setCategory(string newValue) { updateValue(category, newValue, DataIndex::category); } virtual void setAlive(bool newValue) { updateValue(alive, newValue, DataIndex::alive); } @@ -240,26 +242,29 @@ protected: unsigned char shotsIntensity = 2; unsigned char health = 100; double timeToNextTasking = 0; - double barrelHeight = 1.0; /* m */ - double muzzleVelocity = 860; /* m/s */ - double aimTime = 10; /* s */ - unsigned int shotsToFire = 10; - double shotsBaseInterval = 15; /* s */ - double shotsBaseScatter = 2; /* degs */ - double engagementRange = 10000; /* m */ - double targetingRange = 0; /* m */ - double aimMethodRange = 0; /* m */ - double acquisitionRange = 0; /* m */ + double barrelHeight = 0; + double muzzleVelocity = 0; + double aimTime = 0; + unsigned int shotsToFire = 0; + double shotsBaseInterval = 0; + double shotsBaseScatter = 0; + double engagementRange = 0; + double targetingRange = 0; + double aimMethodRange = 0; + double acquisitionRange = 0; /********** Other **********/ unsigned int taskCheckCounter = 0; - unsigned int internalCounter = 0; Unit* missOnPurposeTarget = nullptr; bool hasTaskAssigned = false; double initialFuel = 0; map updateTimeMap; unsigned long long lastLoopTime = 0; bool enableTaskFailedCheck = false; + unsigned long nextTaskingMilliseconds = 0; + unsigned int totalShellsFired = 0; + unsigned int shellsFiredAtTasking = 0; + unsigned int oldAmmo = 0; /********** Private methods **********/ virtual void AIloop() = 0; diff --git a/backend/core/include/weapon.h b/backend/core/include/weapon.h index 31d65b84..2fed5f08 100644 --- a/backend/core/include/weapon.h +++ b/backend/core/include/weapon.h @@ -113,4 +113,10 @@ class Bomb : public Weapon { public: Bomb(json::value json, unsigned int ID); +}; + +class Shell : public Weapon +{ +public: + Shell(json::value json, unsigned int ID); }; \ No newline at end of file diff --git a/backend/core/src/groundunit.cpp b/backend/core/src/groundunit.cpp index 194bdfb0..3923ce03 100644 --- a/backend/core/src/groundunit.cpp +++ b/backend/core/src/groundunit.cpp @@ -168,6 +168,18 @@ void GroundUnit::setState(unsigned char newState) void GroundUnit::AIloop() { srand(static_cast(time(NULL)) + ID); + unsigned long timeNow = std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1); + + double currentAmmo = computeTotalAmmo(); + /* Out of ammo */ + if (currentAmmo <= shotsToFire && state != State::IDLE) { + setState(State::IDLE); + } + + /* Account for unit reloading */ + if (currentAmmo < oldAmmo) + totalShellsFired += oldAmmo - currentAmmo; + oldAmmo = currentAmmo; switch (state) { case State::IDLE: { @@ -241,7 +253,11 @@ void GroundUnit::AIloop() case State::SIMULATE_FIRE_FIGHT: { string taskString = ""; - if (internalCounter == 0 && targetPosition != Coords(NULL) && scheduler->getLoad() < 30) { + if ( + (totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) && + targetPosition != Coords(NULL) && + scheduler->getLoad() < 30 + ) { /* Get the distance and bearing to the target */ Coords scatteredTargetPosition = targetPosition; double distance; @@ -249,9 +265,12 @@ void GroundUnit::AIloop() double bearing2; Geodesic::WGS84().Inverse(getPosition().lat, getPosition().lng, scatteredTargetPosition.lat, scatteredTargetPosition.lng, distance, bearing1, bearing2); + /* Apply a scatter to the aim */ + bearing1 += RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter + 1) * 10; + /* Compute the scattered position applying a random scatter to the shot */ double scatterDistance = distance * tan(10 /* degs */ * (ShotsScatter::LOW - shotsScatter) / 57.29577 + 2 / 57.29577 /* degs */) * RANDOM_MINUS_ONE_TO_ONE; - Geodesic::WGS84().Direct(scatteredTargetPosition.lat, scatteredTargetPosition.lng, bearing1 + 90, scatterDistance, scatteredTargetPosition.lat, scatteredTargetPosition.lng); + Geodesic::WGS84().Direct(scatteredTargetPosition.lat, scatteredTargetPosition.lng, bearing1, scatterDistance, scatteredTargetPosition.lat, scatteredTargetPosition.lng); /* Recover the data from the database */ bool indirectFire = false; @@ -267,9 +286,10 @@ void GroundUnit::AIloop() log(unitName + "(" + name + ")" + " simulating fire fight with indirect fire"); std::ostringstream taskSS; taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << scatteredTargetPosition.lat << ", lng = " << scatteredTargetPosition.lng << ", radius = 100}"; + taskSS << "{id = 'FireAtPoint', lat = " << scatteredTargetPosition.lat << ", lng = " << scatteredTargetPosition.lng << ", radius = 0.01}"; Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; setHasTask(true); } /* Otherwise use the aim method */ @@ -281,18 +301,17 @@ void GroundUnit::AIloop() } /* Wait an amout of time depending on the shots intensity */ - internalCounter = static_cast(((ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + aimTime) / FRAMERATE_TIME_INTERVAL); + nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); } if (targetPosition == Coords(NULL)) setState(State::IDLE); /* Fallback if something went wrong */ - if (internalCounter == 0) - internalCounter = static_cast(3 / FRAMERATE_TIME_INTERVAL); - internalCounter--; + if (timeNow >= nextTaskingMilliseconds) + nextTaskingMilliseconds = timeNow + static_cast(3 * 1000); - setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL); + setTimeToNextTasking(((nextTaskingMilliseconds - timeNow) / 1000.0)); if (taskString.length() > 0) setTask(taskString); @@ -303,7 +322,8 @@ void GroundUnit::AIloop() string taskString = ""; /* Only perform scenic functions when the scheduler is "free" */ - if (((!getHasTask() && scheduler->getLoad() < 30) || internalCounter == 0)) { + if ((totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) && + scheduler->getLoad() < 30) { double distance = 0; unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition; unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2; @@ -341,17 +361,18 @@ void GroundUnit::AIloop() taskString += "Scenic AAA. Bearing: " + to_string((int)round(randomBearing)) + "deg"; } - taskString += ". Aim point elevation " + to_string((int) round(barrelElevation)) + "m AGL"; + taskString += ". Aim point elevation " + to_string((int) round(barrelElevation - position.alt)) + "m AGL"; std::ostringstream taskSS; taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << barrelElevation << ", radius = 0.001, expendQty = " << shotsToFire << " }"; + taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << barrelElevation << ", radius = 0.001 }"; Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; setHasTask(true); /* Wait an amout of time depending on the shots intensity */ - internalCounter = static_cast(((ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + aimTime) / FRAMERATE_TIME_INTERVAL); + nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); } else { if (target == nullptr) @@ -361,11 +382,10 @@ void GroundUnit::AIloop() } } - if (internalCounter == 0) - internalCounter = static_cast(3 / FRAMERATE_TIME_INTERVAL); - internalCounter--; + if (timeNow >= nextTaskingMilliseconds) + nextTaskingMilliseconds = timeNow + static_cast(3 * 1000); - setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL); + setTimeToNextTasking((nextTaskingMilliseconds - timeNow) / 1000.0); if (taskString.length() > 0) setTask(taskString); @@ -385,7 +405,8 @@ void GroundUnit::AIloop() if (canAAA) { /* Only perform scenic functions when the scheduler is "free" */ /* Only run this when the internal counter reaches 0 to avoid excessive computations when no nearby target */ - if (scheduler->getLoad() < 30 && internalCounter == 0) { + if ((totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) && + scheduler->getLoad() < 30) { double distance = 0; unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition; unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2; @@ -422,9 +443,10 @@ void GroundUnit::AIloop() taskSS << "{id = 'AttackUnit', unitID = " << target->getID() << " }"; Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; setHasTask(true); - internalCounter = static_cast((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); + nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); } /* Else, do miss on purpose */ else { @@ -443,14 +465,15 @@ void GroundUnit::AIloop() /* If the unit is closer than the engagement range, use the fire at point method */ std::ostringstream taskSS; taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << aimLat << ", lng = " << aimLng << ", alt = " << aimAlt << ", radius = 0.001, expendQty = " << shotsToFire << " }"; + taskSS << "{id = 'FireAtPoint', lat = " << aimLat << ", lng = " << aimLng << ", alt = " << aimAlt << ", radius = 0.001 }"; taskString += ". Aiming altitude " + to_string((int)round((aimAlt - position.alt) / 0.3048)) + "ft AGL"; Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; setHasTask(true); setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); - internalCounter = static_cast((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); + nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); } else if (distance < aimMethodRange) { taskString += ". Range is less than aim method range (" + to_string((int)round(aimMethodRange / 0.3048)) + "ft), using AIM method."; @@ -460,7 +483,7 @@ void GroundUnit::AIloop() taskString += aimMethodTask; setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); - internalCounter = static_cast((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); + nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); } else { taskString += ". Target is not in range of weapon, waking up unit to get ready for tasking."; @@ -471,11 +494,12 @@ void GroundUnit::AIloop() taskSS << "{id = 'FireAtPoint', lat = " << 0 << ", lng = " << 0 << ", alt = " << 0 << ", radius = 0.001, expendQty = " << 0 << " }"; Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; setHasTask(true); setTargetPosition(Coords(NULL)); /* Don't wait too long before checking again */ - internalCounter = static_cast(5 / FRAMERATE_TIME_INTERVAL); + nextTaskingMilliseconds = timeNow + static_cast(5 * 1000); } } missOnPurposeTarget = target; @@ -488,24 +512,24 @@ void GroundUnit::AIloop() } /* If no valid target was detected */ - if (internalCounter == 0) { + if (timeNow >= nextTaskingMilliseconds) { double alertnessTimeConstant = 10; /* s */ if (database.has_object_field(to_wstring(name))) { json::value databaseEntry = database[to_wstring(name)]; if (databaseEntry.has_number_field(L"alertnessTimeConstant")) alertnessTimeConstant = databaseEntry[L"alertnessTimeConstant"].as_number().to_double(); } - internalCounter = static_cast((5 + RANDOM_ZERO_TO_ONE * alertnessTimeConstant) / FRAMERATE_TIME_INTERVAL); + nextTaskingMilliseconds = timeNow + static_cast((5 + RANDOM_ZERO_TO_ONE * alertnessTimeConstant) * 1000L); missOnPurposeTarget = nullptr; setTargetPosition(Coords(NULL)); } - internalCounter--; + } else { setState(State::IDLE); } - setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL); + setTimeToNextTasking((nextTaskingMilliseconds - timeNow) / 1000.0); if (taskString.length() > 0) setTask(taskString); @@ -528,20 +552,6 @@ string GroundUnit::aimAtPoint(Coords aimTarget) { /* Aim point distance */ double r = 15; /* m */ - /* Default gun values */ - double barrelHeight = 1.0; /* m */ - double muzzleVelocity = 860; /* m/s */ - double shotsBaseScatter = 5; /* degs */ - if (database.has_object_field(to_wstring(name))) { - json::value databaseEntry = database[to_wstring(name)]; - if (databaseEntry.has_number_field(L"barrelHeight") && databaseEntry.has_number_field(L"muzzleVelocity")) { - barrelHeight = databaseEntry[L"barrelHeight"].as_number().to_double(); - muzzleVelocity = databaseEntry[L"muzzleVelocity"].as_number().to_double(); - } - if (databaseEntry.has_number_field(L"shotsBaseScatter")) - shotsBaseScatter = databaseEntry[L"shotsBaseScatter"].as_number().to_double(); - } - /* Compute the elevation angle of the gun*/ double deltaHeight = (aimTarget.alt - (position.alt + barrelHeight)); double alpha = 9.81 / 2 * dist * dist / (muzzleVelocity * muzzleVelocity); @@ -564,6 +574,7 @@ string GroundUnit::aimAtPoint(Coords aimTarget) { taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << position.alt + barrelElevation + barrelHeight << ", radius = 0.001}"; Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; setHasTask(true); } else { diff --git a/backend/core/src/unit.cpp b/backend/core/src/unit.cpp index 48e1383f..32e72846 100644 --- a/backend/core/src/unit.cpp +++ b/backend/core/src/unit.cpp @@ -825,3 +825,11 @@ void Unit::setHasTaskAssigned(bool newHasTaskAssigned) { void Unit::triggerUpdate(unsigned char datumIndex) { updateTimeMap[datumIndex] = duration_cast(system_clock::now().time_since_epoch()).count(); } + +unsigned int Unit::computeTotalAmmo() +{ + unsigned int totalShells = 0; + for (auto const& ammoItem : ammo) + totalShells += ammoItem.quantity; + return totalShells; +} diff --git a/backend/core/src/weapon.cpp b/backend/core/src/weapon.cpp index d826bacb..c1e77712 100644 --- a/backend/core/src/weapon.cpp +++ b/backend/core/src/weapon.cpp @@ -113,4 +113,11 @@ Bomb::Bomb(json::value json, unsigned int ID) : Weapon(json, ID) { log("New Bomb created with ID: " + to_string(ID)); setCategory("Bomb"); +}; + +/* Shell */ +Shell::Shell(json::value json, unsigned int ID) : Weapon(json, ID) +{ + log("New Shell created with ID: " + to_string(ID)); + setCategory("Shell"); }; \ No newline at end of file diff --git a/backend/core/src/weaponsmanager.cpp b/backend/core/src/weaponsmanager.cpp index cc0c21c3..5343456a 100644 --- a/backend/core/src/weaponsmanager.cpp +++ b/backend/core/src/weaponsmanager.cpp @@ -41,6 +41,8 @@ void WeaponsManager::update(json::value& json, double dt) weapons[ID] = dynamic_cast(new Missile(p.second, ID)); else if (category.compare("Bomb") == 0) weapons[ID] = dynamic_cast(new Bomb(p.second, ID)); + else if (category.compare("Shell") == 0) + weapons[ID] = dynamic_cast(new Shell(p.second, ID)); /* Initialize the weapon if creation was successfull */ if (weapons.count(ID) != 0) { diff --git a/databases/units/groundunitdatabase.json b/databases/units/groundunitdatabase.json index 43537996..e140977a 100644 --- a/databases/units/groundunitdatabase.json +++ b/databases/units/groundunitdatabase.json @@ -129,7 +129,7 @@ "canRearm": false, "muzzleVelocity": 1000, "barrelHeight": 2, - "aimTime": 5, + "aimTime": 7, "shotsToFire": 10, "cost": null, "tags": "Optical, Radar, CA", @@ -587,7 +587,7 @@ "abilities": "Combined arms, Amphibious, Transport, AA", "canTargetPoint": true, "canRearm": false, - "aimTime": 5, + "aimTime": 6, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -819,7 +819,7 @@ "canRearm": false, "barrelHeight": 2.2, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 6, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -955,7 +955,7 @@ "acquisitionRange": 3000, "engagementRange": 1000, "description": "Armoured car, MRAP. Wheeled. Amphibious. 12.7 mm machine gun.", - "abilities": "Combined arms, Amphibious, AA", + "abilities": "Combined arms, Amphibious", "canTargetPoint": true, "canRearm": false, "barrelHeight": 2, @@ -969,7 +969,7 @@ "shotsBaseInterval": 6, "shotsBaseScatter": 10, "alertnessTimeConstant": 4, - "canAAA": true + "canAAA": false }, "Dog Ear radar": { "name": "Dog Ear radar", @@ -1188,7 +1188,7 @@ "abilities": "Combined arms, AA", "canTargetPoint": true, "canRearm": false, - "aimTime": 8, + "aimTime": 7, "shotsToFire": 5, "cost": null, "tags": "Radar, CA", @@ -2280,7 +2280,7 @@ "abilities": "AA, Embark", "canTargetPoint": true, "canRearm": false, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "tags": "Russian type 1", "markerFile": "groundunit-infantry", @@ -2789,7 +2789,7 @@ "shotsBaseInterval": 6, "shotsToFire": 5, "tags": "CA", - "aimTime": 5, + "aimTime": 7, "aimMethodRange": 3000, "shotsBaseScatter": 5, "alertnessTimeConstant": 4, @@ -2931,7 +2931,7 @@ "aimMethodRange": 300, "shotsBaseScatter": 5, "alertnessTimeConstant": 3, - "canAAA": true + "canAAA": false }, "M-109": { "name": "M-109", @@ -3096,7 +3096,7 @@ "canRearm": false, "barrelHeight": 2.8, "muzzleVelocity": 950, - "aimTime": 5, + "aimTime": 2.5, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -3265,7 +3265,7 @@ "markerFile": "groundunit-tactical", "targetingRange": 100, "aimMethodRange": 2500, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "shotsBaseInterval": 5, "shotsBaseScatter": 5, @@ -3368,7 +3368,7 @@ "canRearm": false, "barrelHeight": 3, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 2.7, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -3800,7 +3800,7 @@ "canRearm": false, "barrelHeight": 2.05, "muzzleVelocity": 800, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -3845,13 +3845,13 @@ }, "acquisitionRange": 3000, "engagementRange": 1000, - "description": "Marder Infantry FIghting Vehicle. Tracked. Amphibious. 20 mm gun and 7.62 mm machine gun.", + "description": "Marder Infantry Fighting Vehicle. Tracked. Amphibious. 20 mm gun and 7.62 mm machine gun.", "abilities": "Combined arms, Transport, Amphibious", "canTargetPoint": true, "canRearm": false, "barrelHeight": 2.82, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 9, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -3917,7 +3917,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Russian paratrooper carrying AKS-74.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "barrelHeight": 0.9, @@ -3926,7 +3926,7 @@ "shotsToFire": 5, "tags": "Russian Para", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 100, "aimMethodRange": 2000, "shotsBaseInterval": 5, @@ -3955,7 +3955,7 @@ "shotsToFire": 1, "tags": "Russian Para", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 50, "aimMethodRange": 750, "shotsBaseInterval": 5, @@ -5613,7 +5613,7 @@ "abilities": "AA, Embark", "canTargetPoint": true, "canRearm": false, - "aimTime": 5, + "aimTime": 2.5, "shotsToFire": 5, "tags": "Russian type 4", "markerFile": "groundunit-infantry", @@ -5679,7 +5679,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Solider carrying M249.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "muzzleVelocity": 915, @@ -5688,7 +5688,7 @@ "barrelHeight": 0.25, "tags": "US", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 100, "aimMethodRange": 2000, "shotsBaseInterval": 5, @@ -5758,7 +5758,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Solider carrying M4.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "barrelHeight": 0.95, @@ -5767,7 +5767,7 @@ "shotsToFire": 5, "tags": "Georgia", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 100, "aimMethodRange": 2000, "shotsBaseInterval": 5, @@ -5829,7 +5829,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Solider carrying M4.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "barrelHeight": 1, @@ -5838,7 +5838,7 @@ "shotsToFire": 5, "tags": "US", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 100, "aimMethodRange": 2000, "shotsBaseInterval": 5, @@ -5909,7 +5909,7 @@ "shotsToFire": 1, "tags": "Russian", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 50, "aimMethodRange": 750, "shotsBaseInterval": 5, @@ -6200,7 +6200,7 @@ "aimMethodRange": 2500, "shotsBaseScatter": 5, "alertnessTimeConstant": 6, - "canAAA": true + "canAAA": false }, "T-72B": { "name": "T-72B", @@ -6234,7 +6234,7 @@ "muzzleVelocity": 700, "aimMethodRange": 2500, "alertnessTimeConstant": 4, - "canAAA": true, + "canAAA": false, "tags": "CA" }, "T-80UD": { @@ -6310,7 +6310,7 @@ "aimMethodRange": 3000, "shotsBaseScatter": 5, "alertnessTimeConstant": 5, - "canAAA": true + "canAAA": false }, "T-90": { "name": "T-90", @@ -6374,7 +6374,7 @@ "shotsToFire": 5, "shotsBaseInterval": 8, "tags": "CA", - "canAAA": true, + "canAAA": false, "alertnessTimeConstant": 3, "shotsBaseScatter": 5, "aimMethodRange": 3000 @@ -6406,7 +6406,7 @@ "shotsBaseInterval": 5, "shotsBaseScatter": 5, "alertnessTimeConstant": 5, - "canAAA": true + "canAAA": false }, "Tigr_233036": { "name": "Tigr_233036", @@ -6757,7 +6757,7 @@ "canTargetPoint": true, "canRearm": false, "shotsToFire": 5, - "aimTime": 8, + "aimTime": 6, "muzzleVelocity": 1000, "barrelHeight": 3, "cost": null, @@ -6795,7 +6795,7 @@ "cost": null, "barrelHeight": 3, "muzzleVelocity": 1000, - "aimTime": 8, + "aimTime": 6, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-aaa", @@ -7028,7 +7028,7 @@ "cost": null, "barrelHeight": 2.5, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 8, "shotsToFire": 5, "tags": "Radar, CA", "markerFile": "groundunit-aaa", @@ -7240,7 +7240,7 @@ "canRearm": false, "barrelHeight": 1.8, "muzzleVelocity": 1000, - "aimTime": 9, + "aimTime": 13, "shotsToFire": 5, "cost": null, "tags": "Radar, CA", @@ -7325,7 +7325,7 @@ "canTargetPoint": true, "canRearm": false, "shotsToFire": 5, - "aimTime": 9, + "aimTime": 6, "muzzleVelocity": 1000, "barrelHeight": 1.5, "cost": null, @@ -7377,7 +7377,7 @@ "canTargetPoint": true, "canRearm": false, "shotsToFire": 5, - "aimTime": 9, + "aimTime": 6, "muzzleVelocity": 1000, "barrelHeight": 1.5, "cost": null, @@ -7413,7 +7413,7 @@ "canTargetPoint": true, "canRearm": false, "shotsToFire": 5, - "aimTime": 9, + "aimTime": 6, "muzzleVelocity": 1000, "barrelHeight": 1.5, "cost": null, @@ -7795,7 +7795,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Insurgent solider carrying AK-74.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "barrelHeight": 0.9, @@ -7804,7 +7804,7 @@ "shotsToFire": 5, "tags": "Insurgent", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "aimMethodRange": 2000, "targetingRange": 100, "shotsBaseInterval": 5, @@ -7958,7 +7958,7 @@ "canRearm": false, "barrelHeight": 0.9, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "tags": "Russian type 2", "markerFile": "groundunit-infantry", @@ -8047,7 +8047,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Russian solider carrying AK-74.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "barrelHeight": 0.9, @@ -8056,7 +8056,7 @@ "shotsToFire": 5, "tags": "Russian type 3", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "aimMethodRange": 2000, "targetingRange": 100, "shotsBaseInterval": 5, @@ -8227,7 +8227,7 @@ "tags": "CA", "aimMethodRange": 3000, "shotsBaseScatter": 5, - "canAAA": true, + "canAAA": false, "alertnessTimeConstant": 3 }, "LiAZ Bus": { @@ -8619,24 +8619,25 @@ "countries": "All" } }, - "acquisitionRange": 16000, - "engagementRange": 2000, + "acquisitionRange": 30000, + "engagementRange": 21000, "description": "KS-19. 100 mm AAA gun. Fixed manually aimed large calibre anti aircraft gun.", "abilities": "AA", "canTargetPoint": false, "canRearm": false, "muzzleVelocity": 1000, - "aimTime": 25, + "aimTime": 15, "shotsToFire": 5, "barrelHeight": 5, "cost": null, "markerFile": "groundunit-aaa", "canAAA": true, "targetingRange": 100, - "aimMethodRange": 15000, + "aimMethodRange": 100, "shotsBaseInterval": 5, "shotsBaseScatter": 5, - "alertnessTimeConstant": 5 + "alertnessTimeConstant": 5, + "flak": true }, "SON_9": { "name": "SON_9", @@ -8969,7 +8970,7 @@ "cost": null, "barrelHeight": 2, "muzzleVelocity": 1000, - "aimTime": 5, + "aimTime": 6, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-aaa", @@ -9016,7 +9017,7 @@ "cost": null, "barrelHeight": 2, "muzzleVelocity": 1000, - "aimTime": 5, + "aimTime": 6, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-aaa", @@ -9329,7 +9330,7 @@ "canRearm": false, "barrelHeight": 2.6, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -9520,7 +9521,7 @@ "canRearm": false, "muzzleVelocity": 1200, "barrelHeight": 3, - "aimTime": 20, + "aimTime": 10, "shotsToFire": 5, "cost": null, "tags": "CA", @@ -9558,7 +9559,7 @@ "canTargetPoint": true, "canRearm": false, "muzzleVelocity": 1000, - "aimTime": 5, + "aimTime": 10, "shotsToFire": 5, "barrelHeight": 2, "cost": null, @@ -9645,7 +9646,7 @@ "barrelHeight": 2.7, "shotsBaseInterval": 8, "shotsToFire": 5, - "aimTime": 5, + "aimTime": 3, "shotsBaseScatter": 5, "alertnessTimeConstant": 3, "aimMethodRange": 3000, @@ -9771,7 +9772,7 @@ "canRearm": false, "barrelHeight": 2.8, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 4, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -10634,7 +10635,7 @@ "countries": "All" } }, - "aimTime": 25, + "aimTime": 15, "shotsToFire": 2, "acquisitionRange": 15000, "engagementRange": 12000, @@ -10649,9 +10650,10 @@ "canAAA": true, "shotsBaseInterval": 10, "shotsBaseScatter": 5, - "aimMethodRange": 15000, - "targetingRange": 200, - "alertnessTimeConstant": 15 + "aimMethodRange": 100, + "targetingRange": 100, + "alertnessTimeConstant": 15, + "flak": true }, "Pz_IV_H": { "name": "Pz_IV_H", @@ -10768,7 +10770,7 @@ "aimMethodRange": 3000, "barrelHeight": 2.7, "muzzleVelocity": 700, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "shotsBaseInterval": 6, "tags": "CA", @@ -10879,7 +10881,7 @@ "barrelHeight": 2.7, "shotsBaseInterval": 7, "tags": "CA", - "aimTime": 5, + "aimTime": 6, "shotsToFire": 5, "aimMethodRange": 3000, "shotsBaseScatter": 5, @@ -10957,7 +10959,7 @@ "barrelHeight": 2.7, "shotsBaseInterval": 7, "shotsToFire": 5, - "aimTime": 5, + "aimTime": 8, "tags": "CA", "aimMethodRange": 3000, "shotsBaseScatter": 5, @@ -11195,7 +11197,7 @@ "alertnessTimeConstant": 3, "shotsBaseScatter": 5, "aimMethodRange": 3000, - "canAAA": true + "canAAA": false }, "ZBD04A": { "name": "ZBD04A", @@ -11357,7 +11359,7 @@ "aimMethodRange": 3000, "shotsBaseScatter": 5, "alertnessTimeConstant": 6, - "canAAA": true + "canAAA": false }, "Kubelwagen_82": { "name": "Kubelwagen_82", @@ -11860,7 +11862,7 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": false }, "Flakscheinwerfer_37": { "name": "Flakscheinwerfer_37", @@ -11896,7 +11898,8 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": true, + "flak": true }, "Maschinensatz_33": { "name": "Maschinensatz_33", @@ -11932,7 +11935,7 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": false }, "soldier_mauser98": { "name": "soldier_mauser98", @@ -12516,7 +12519,8 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": true, + "aimTime": 6 }, "Allies_Director": { "name": "Allies_Director", @@ -12886,7 +12890,8 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": true, + "aimTime": 6 }, "M1_37mm": { "name": "M1_37mm", @@ -12922,7 +12927,8 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": true, + "aimTime": 5 }, "DR_50Ton_Flat_Wagon": { "name": "DR_50Ton_Flat_Wagon", @@ -13069,7 +13075,8 @@ "name": "Winter", "countries": "All" } - } + }, + "flak": true }, "flak36": { "name": "flak36", @@ -13097,7 +13104,8 @@ "name": "Winter", "countries": "All" } - } + }, + "flak": true }, "flak37": { "name": "flak37", @@ -13125,7 +13133,8 @@ "name": "Winter", "countries": "All" } - } + }, + "flak": true }, "flak38": { "name": "flak38", @@ -13153,7 +13162,8 @@ "name": "Winter", "countries": "All" } - } + }, + "flak": true }, "flak41": { "name": "flak41", @@ -13181,7 +13191,8 @@ "name": "Winter", "countries": "All" } - } + }, + "flak": true }, "HEMTT_C-RAM_Phalanx": { "name": "HEMTT_C-RAM_Phalanx", diff --git a/frontend/react/public/images/units/map/awacs/blue/shell.svg b/frontend/react/public/images/units/map/awacs/blue/shell.svg new file mode 100644 index 00000000..72ebcfb5 --- /dev/null +++ b/frontend/react/public/images/units/map/awacs/blue/shell.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/frontend/react/public/images/units/map/awacs/neutral/shell.svg b/frontend/react/public/images/units/map/awacs/neutral/shell.svg new file mode 100644 index 00000000..72ebcfb5 --- /dev/null +++ b/frontend/react/public/images/units/map/awacs/neutral/shell.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/frontend/react/public/images/units/map/normal/blue/shell.svg b/frontend/react/public/images/units/map/normal/blue/shell.svg new file mode 100644 index 00000000..72ebcfb5 --- /dev/null +++ b/frontend/react/public/images/units/map/normal/blue/shell.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/frontend/react/public/images/units/map/normal/neutral/shell.svg b/frontend/react/public/images/units/map/normal/neutral/shell.svg new file mode 100644 index 00000000..72ebcfb5 --- /dev/null +++ b/frontend/react/public/images/units/map/normal/neutral/shell.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/frontend/react/public/images/units/map/normal/red/shell.svg b/frontend/react/public/images/units/map/normal/red/shell.svg new file mode 100644 index 00000000..72ebcfb5 --- /dev/null +++ b/frontend/react/public/images/units/map/normal/red/shell.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 1b50c1a5..bee24c70 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -507,11 +507,6 @@ export class Map extends L.Map { altKey: false, ctrlKey: false, }); - - /* Periodically check if the camera control endpoint is available */ - this.#cameraControlTimer = window.setInterval(() => { - this.#checkCameraPort(); - }, 1000); } setLayerName(layerName: string) { @@ -1298,33 +1293,6 @@ export class Map extends L.Map { return minimapBoundaries; } - #setSlaveDCSCameraAvailable(newSlaveDCSCameraAvailable: boolean) { - this.#slaveDCSCameraAvailable = newSlaveDCSCameraAvailable; - } - - /* Check if the camera control plugin is available. Right now this will only change the color of the button, no changes in functionality */ - #checkCameraPort() { - if (this.#cameraOptionsXmlHttp?.readyState !== 4) this.#cameraOptionsXmlHttp?.abort(); - - this.#cameraOptionsXmlHttp = new XMLHttpRequest(); - - /* Using 127.0.0.1 instead of localhost because the LuaSocket version used in DCS only listens to IPv4. This avoids the lag caused by the - browser if it first tries to send the request on the IPv6 address for localhost */ - this.#cameraOptionsXmlHttp.open("OPTIONS", `http://127.0.0.1:${this.#cameraControlPort}`); - this.#cameraOptionsXmlHttp.onload = (res: any) => { - if (this.#cameraOptionsXmlHttp !== null && this.#cameraOptionsXmlHttp.status == 204) this.#setSlaveDCSCameraAvailable(true); - else this.#setSlaveDCSCameraAvailable(false); - }; - this.#cameraOptionsXmlHttp.onerror = (res: any) => { - this.#setSlaveDCSCameraAvailable(false); - }; - this.#cameraOptionsXmlHttp.ontimeout = (res: any) => { - this.#setSlaveDCSCameraAvailable(false); - }; - this.#cameraOptionsXmlHttp.timeout = 500; - this.#cameraOptionsXmlHttp.send(""); - } - #drawIPToTargetLine() { if (this.#targetPoint && this.#IPPoint) { if (!this.#IPToTargetLine) { diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 8be5071b..917e33bc 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -188,6 +188,8 @@ export abstract class Unit extends CustomMarker { #targetingRange: number = 0; #aimMethodRange: number = 0; #acquisitionRange: number = 0; + #totalAmmo: number = 0; + #previousTotalAmmo: number = 0; /* Inputs timers */ #debounceTimeout: number | null = null; @@ -654,6 +656,8 @@ export abstract class Unit extends CustomMarker { break; case DataIndexes.ammo: this.#ammo = dataExtractor.extractAmmo(); + this.#previousTotalAmmo = this.#totalAmmo; + this.#totalAmmo = this.#ammo.reduce((prev: number, ammo: Ammo) => prev + ammo.quantity, 0); break; case DataIndexes.contacts: this.#contacts = dataExtractor.extractContacts(); diff --git a/frontend/react/src/weapon/weapon.ts b/frontend/react/src/weapon/weapon.ts index b37bad0d..0181c9e0 100644 --- a/frontend/react/src/weapon/weapon.ts +++ b/frontend/react/src/weapon/weapon.ts @@ -43,6 +43,7 @@ export abstract class Weapon extends CustomMarker { static getConstructor(type: string) { if (type === "Missile") return Missile; if (type === "Bomb") return Bomb; + if (type === "Shell") return Shell; } constructor(ID: number) { @@ -330,3 +331,40 @@ export class Bomb extends Weapon { }; } } + +export class Shell extends Weapon { + constructor(ID: number) { + super(ID); + } + + getCategory() { + return "Shell"; + } + + getMarkerCategory() { + if (this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC)) return "shell"; + else return "aircraft"; + } + + getIconOptions() { + return { + showState: false, + showVvi: + !this.belongsToCommandedCoalition() && + !this.getDetectionMethods().some((value) => [VISUAL, OPTIC].includes(value)) && + this.getDetectionMethods().some((value) => [RADAR, IRST, DLINK].includes(value)), + showHealth: false, + showHotgroup: false, + showUnitIcon: this.belongsToCommandedCoalition() || this.getDetectionMethods().some((value) => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value)), + showShortLabel: false, + showFuel: false, + showAmmo: false, + showSummary: + !this.belongsToCommandedCoalition() && + !this.getDetectionMethods().some((value) => [VISUAL, OPTIC].includes(value)) && + this.getDetectionMethods().some((value) => [RADAR, IRST, DLINK].includes(value)), + showCallsign: false, + rotateToHeading: this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC), + }; + } +} diff --git a/frontend/react/src/weapon/weaponsmanager.ts b/frontend/react/src/weapon/weaponsmanager.ts index c328c98f..f308c833 100644 --- a/frontend/react/src/weapon/weaponsmanager.ts +++ b/frontend/react/src/weapon/weaponsmanager.ts @@ -38,7 +38,7 @@ export class WeaponsManager { /** Add a new weapon to the manager * * @param ID ID of the new weapon - * @param category Either "Missile" or "Bomb". Determines what class will be used to create the new unit accordingly. + * @param category Either "Missile", "Bomb" or "Shell". Determines what class will be used to create the new unit accordingly. */ addWeapon(ID: number, category: string) { if (category) { diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua index af9f1ce9..4a0d2385 100644 --- a/scripts/lua/backend/OlympusCommand.lua +++ b/scripts/lua/backend/OlympusCommand.lua @@ -1415,6 +1415,8 @@ function Olympus.setWeaponsData(arg, time) table["category"] = "Missile" elseif weapon:getDesc().category == Weapon.Category.BOMB then table["category"] = "Bomb" + elseif weapon:getDesc().category == Weapon.Category.SHELL then + table["category"] = "Shell" end else weapons[ID] = {isAlive = false} From 3d33319a192c14b9b099e7f0e490de60418bada8 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Fri, 14 Mar 2025 18:43:10 +0100 Subject: [PATCH 16/33] fix: minor changes to databases --- databases/units/groundunitdatabase.json | 147 ++++++++++++------------ 1 file changed, 75 insertions(+), 72 deletions(-) diff --git a/databases/units/groundunitdatabase.json b/databases/units/groundunitdatabase.json index 186d2f54..596feb34 100644 --- a/databases/units/groundunitdatabase.json +++ b/databases/units/groundunitdatabase.json @@ -129,7 +129,7 @@ "canRearm": false, "muzzleVelocity": 1000, "barrelHeight": 2, - "aimTime": 5, + "aimTime": 7, "shotsToFire": 10, "cost": null, "tags": "Optical, Radar, CA", @@ -587,7 +587,7 @@ "abilities": "Combined arms, Amphibious, Transport, AA", "canTargetPoint": true, "canRearm": false, - "aimTime": 5, + "aimTime": 6, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -819,7 +819,7 @@ "canRearm": false, "barrelHeight": 2.2, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 6, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -955,7 +955,7 @@ "acquisitionRange": 3000, "engagementRange": 1000, "description": "Armoured car, MRAP. Wheeled. Amphibious. 12.7 mm machine gun.", - "abilities": "Combined arms, Amphibious, AA", + "abilities": "Combined arms, Amphibious", "canTargetPoint": true, "canRearm": false, "barrelHeight": 2, @@ -969,7 +969,7 @@ "shotsBaseInterval": 6, "shotsBaseScatter": 10, "alertnessTimeConstant": 4, - "canAAA": true + "canAAA": false }, "Dog Ear radar": { "name": "Dog Ear radar", @@ -1188,7 +1188,7 @@ "abilities": "Combined arms, AA", "canTargetPoint": true, "canRearm": false, - "aimTime": 8, + "aimTime": 7, "shotsToFire": 5, "cost": null, "tags": "Radar, CA", @@ -2280,7 +2280,7 @@ "abilities": "AA, Embark", "canTargetPoint": true, "canRearm": false, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "tags": "Russian type 1", "markerFile": "groundunit-infantry", @@ -2789,7 +2789,7 @@ "shotsBaseInterval": 6, "shotsToFire": 5, "tags": "CA", - "aimTime": 5, + "aimTime": 7, "aimMethodRange": 3000, "shotsBaseScatter": 5, "alertnessTimeConstant": 4, @@ -2931,7 +2931,7 @@ "aimMethodRange": 300, "shotsBaseScatter": 5, "alertnessTimeConstant": 3, - "canAAA": true + "canAAA": false }, "M-109": { "name": "M-109", @@ -3096,7 +3096,7 @@ "canRearm": false, "barrelHeight": 2.8, "muzzleVelocity": 950, - "aimTime": 5, + "aimTime": 2.5, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -3265,7 +3265,7 @@ "markerFile": "groundunit-tactical", "targetingRange": 100, "aimMethodRange": 2500, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "shotsBaseInterval": 5, "shotsBaseScatter": 5, @@ -3368,7 +3368,7 @@ "canRearm": false, "barrelHeight": 3, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 2.7, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -3800,7 +3800,7 @@ "canRearm": false, "barrelHeight": 2.05, "muzzleVelocity": 800, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -3845,13 +3845,13 @@ }, "acquisitionRange": 3000, "engagementRange": 1000, - "description": "Marder Infantry FIghting Vehicle. Tracked. Amphibious. 20 mm gun and 7.62 mm machine gun.", + "description": "Marder Infantry Fighting Vehicle. Tracked. Amphibious. 20 mm gun and 7.62 mm machine gun.", "abilities": "Combined arms, Transport, Amphibious", "canTargetPoint": true, "canRearm": false, "barrelHeight": 2.82, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 9, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -3917,7 +3917,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Russian paratrooper carrying AKS-74.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "barrelHeight": 0.9, @@ -3926,7 +3926,7 @@ "shotsToFire": 5, "tags": "Russian Para", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 100, "aimMethodRange": 2000, "shotsBaseInterval": 5, @@ -3955,7 +3955,7 @@ "shotsToFire": 1, "tags": "Russian Para", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 50, "aimMethodRange": 750, "shotsBaseInterval": 5, @@ -5613,7 +5613,7 @@ "abilities": "AA, Embark", "canTargetPoint": true, "canRearm": false, - "aimTime": 5, + "aimTime": 2.5, "shotsToFire": 5, "tags": "Russian type 4", "markerFile": "groundunit-infantry", @@ -5679,7 +5679,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Solider carrying M249.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "muzzleVelocity": 915, @@ -5688,7 +5688,7 @@ "barrelHeight": 0.25, "tags": "US", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 100, "aimMethodRange": 2000, "shotsBaseInterval": 5, @@ -5758,7 +5758,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Solider carrying M4.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "barrelHeight": 0.95, @@ -5767,7 +5767,7 @@ "shotsToFire": 5, "tags": "Georgia", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 100, "aimMethodRange": 2000, "shotsBaseInterval": 5, @@ -5829,7 +5829,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Solider carrying M4.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "barrelHeight": 1, @@ -5838,7 +5838,7 @@ "shotsToFire": 5, "tags": "US", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 100, "aimMethodRange": 2000, "shotsBaseInterval": 5, @@ -5909,7 +5909,7 @@ "shotsToFire": 1, "tags": "Russian", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "targetingRange": 50, "aimMethodRange": 750, "shotsBaseInterval": 5, @@ -6200,7 +6200,7 @@ "aimMethodRange": 2500, "shotsBaseScatter": 5, "alertnessTimeConstant": 6, - "canAAA": true + "canAAA": false }, "T-72B": { "name": "T-72B", @@ -6234,7 +6234,7 @@ "muzzleVelocity": 700, "aimMethodRange": 2500, "alertnessTimeConstant": 4, - "canAAA": true, + "canAAA": false, "tags": "CA" }, "T-80UD": { @@ -6310,7 +6310,7 @@ "aimMethodRange": 3000, "shotsBaseScatter": 5, "alertnessTimeConstant": 5, - "canAAA": true + "canAAA": false }, "T-90": { "name": "T-90", @@ -6374,7 +6374,7 @@ "shotsToFire": 5, "shotsBaseInterval": 8, "tags": "CA", - "canAAA": true, + "canAAA": false, "alertnessTimeConstant": 3, "shotsBaseScatter": 5, "aimMethodRange": 3000 @@ -6406,7 +6406,7 @@ "shotsBaseInterval": 5, "shotsBaseScatter": 5, "alertnessTimeConstant": 5, - "canAAA": true + "canAAA": false }, "Tigr_233036": { "name": "Tigr_233036", @@ -6757,7 +6757,7 @@ "canTargetPoint": true, "canRearm": false, "shotsToFire": 5, - "aimTime": 8, + "aimTime": 6, "muzzleVelocity": 1000, "barrelHeight": 3, "cost": null, @@ -6795,7 +6795,7 @@ "cost": null, "barrelHeight": 3, "muzzleVelocity": 1000, - "aimTime": 8, + "aimTime": 6, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-aaa", @@ -7028,7 +7028,7 @@ "cost": null, "barrelHeight": 2.5, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 8, "shotsToFire": 5, "tags": "Radar, CA", "markerFile": "groundunit-aaa", @@ -7240,7 +7240,7 @@ "canRearm": false, "barrelHeight": 1.8, "muzzleVelocity": 1000, - "aimTime": 9, + "aimTime": 13, "shotsToFire": 5, "cost": null, "tags": "Radar, CA", @@ -7325,7 +7325,7 @@ "canTargetPoint": true, "canRearm": false, "shotsToFire": 5, - "aimTime": 9, + "aimTime": 6, "muzzleVelocity": 1000, "barrelHeight": 1.5, "cost": null, @@ -7377,7 +7377,7 @@ "canTargetPoint": true, "canRearm": false, "shotsToFire": 5, - "aimTime": 9, + "aimTime": 6, "muzzleVelocity": 1000, "barrelHeight": 1.5, "cost": null, @@ -7413,7 +7413,7 @@ "canTargetPoint": true, "canRearm": false, "shotsToFire": 5, - "aimTime": 9, + "aimTime": 6, "muzzleVelocity": 1000, "barrelHeight": 1.5, "cost": null, @@ -7795,7 +7795,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Insurgent solider carrying AK-74.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "barrelHeight": 0.9, @@ -7804,7 +7804,7 @@ "shotsToFire": 5, "tags": "Insurgent", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "aimMethodRange": 2000, "targetingRange": 100, "shotsBaseInterval": 5, @@ -7958,7 +7958,7 @@ "canRearm": false, "barrelHeight": 0.9, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "tags": "Russian type 2", "markerFile": "groundunit-infantry", @@ -8047,7 +8047,7 @@ "acquisitionRange": 2500, "engagementRange": 300, "description": "Russian solider carrying AK-74.", - "abilities": "AA, Embark", + "abilities": "Embark", "canTargetPoint": true, "canRearm": false, "barrelHeight": 0.9, @@ -8056,7 +8056,7 @@ "shotsToFire": 5, "tags": "Russian type 3", "markerFile": "groundunit-infantry", - "canAAA": true, + "canAAA": false, "aimMethodRange": 2000, "targetingRange": 100, "shotsBaseInterval": 5, @@ -8227,7 +8227,7 @@ "tags": "CA", "aimMethodRange": 3000, "shotsBaseScatter": 5, - "canAAA": true, + "canAAA": false, "alertnessTimeConstant": 3 }, "LiAZ Bus": { @@ -8626,16 +8626,16 @@ "canTargetPoint": false, "canRearm": false, "muzzleVelocity": 1000, - "aimTime": 5, + "aimTime": 25, "shotsToFire": 1, "barrelHeight": 5, "cost": null, "markerFile": "groundunit-aaa", "canAAA": true, - "targetingRange": 0, - "aimMethodRange": 0, + "targetingRange": 100, + "aimMethodRange": 100, "shotsBaseInterval": 5, - "shotsBaseScatter": 8, + "shotsBaseScatter": 5, "alertnessTimeConstant": 5, "flak": true }, @@ -8970,7 +8970,7 @@ "cost": null, "barrelHeight": 2, "muzzleVelocity": 1000, - "aimTime": 5, + "aimTime": 6, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-aaa", @@ -9017,7 +9017,7 @@ "cost": null, "barrelHeight": 2, "muzzleVelocity": 1000, - "aimTime": 5, + "aimTime": 6, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-aaa", @@ -9330,7 +9330,7 @@ "canRearm": false, "barrelHeight": 2.6, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -9521,7 +9521,7 @@ "canRearm": false, "muzzleVelocity": 1200, "barrelHeight": 3, - "aimTime": 20, + "aimTime": 10, "shotsToFire": 5, "cost": null, "tags": "CA", @@ -9559,7 +9559,7 @@ "canTargetPoint": true, "canRearm": false, "muzzleVelocity": 1000, - "aimTime": 5, + "aimTime": 10, "shotsToFire": 5, "barrelHeight": 2, "cost": null, @@ -9646,7 +9646,7 @@ "barrelHeight": 2.7, "shotsBaseInterval": 8, "shotsToFire": 5, - "aimTime": 5, + "aimTime": 3, "shotsBaseScatter": 5, "alertnessTimeConstant": 3, "aimMethodRange": 3000, @@ -9772,7 +9772,7 @@ "canRearm": false, "barrelHeight": 2.8, "muzzleVelocity": 900, - "aimTime": 5, + "aimTime": 4, "shotsToFire": 5, "tags": "CA", "markerFile": "groundunit-apc", @@ -10635,8 +10635,8 @@ "countries": "All" } }, - "aimTime": 25, - "shotsToFire": 2, + "aimTime": 15, + "shotsToFire": 1, "acquisitionRange": 15000, "engagementRange": 12000, "description": "The flak 88. Fixed anti aircraft gun famously also used as an anti-tank gun. 88mm flak gun.", @@ -10650,8 +10650,8 @@ "canAAA": true, "shotsBaseInterval": 10, "shotsBaseScatter": 5, - "aimMethodRange": 15000, - "targetingRange": 200, + "aimMethodRange": 100, + "targetingRange": 100, "alertnessTimeConstant": 15, "flak": true }, @@ -10770,7 +10770,7 @@ "aimMethodRange": 3000, "barrelHeight": 2.7, "muzzleVelocity": 700, - "aimTime": 5, + "aimTime": 3, "shotsToFire": 5, "shotsBaseInterval": 6, "tags": "CA", @@ -10881,7 +10881,7 @@ "barrelHeight": 2.7, "shotsBaseInterval": 7, "tags": "CA", - "aimTime": 5, + "aimTime": 6, "shotsToFire": 5, "aimMethodRange": 3000, "shotsBaseScatter": 5, @@ -10959,7 +10959,7 @@ "barrelHeight": 2.7, "shotsBaseInterval": 7, "shotsToFire": 5, - "aimTime": 5, + "aimTime": 8, "tags": "CA", "aimMethodRange": 3000, "shotsBaseScatter": 5, @@ -11197,7 +11197,7 @@ "alertnessTimeConstant": 3, "shotsBaseScatter": 5, "aimMethodRange": 3000, - "canAAA": true + "canAAA": false }, "ZBD04A": { "name": "ZBD04A", @@ -11359,7 +11359,7 @@ "aimMethodRange": 3000, "shotsBaseScatter": 5, "alertnessTimeConstant": 6, - "canAAA": true + "canAAA": false }, "Kubelwagen_82": { "name": "Kubelwagen_82", @@ -11862,7 +11862,7 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": false }, "Flakscheinwerfer_37": { "name": "Flakscheinwerfer_37", @@ -11935,7 +11935,7 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": false }, "soldier_mauser98": { "name": "soldier_mauser98", @@ -12519,7 +12519,8 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": true, + "aimTime": 6 }, "Allies_Director": { "name": "Allies_Director", @@ -12889,7 +12890,8 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": true, + "aimTime": 6 }, "M1_37mm": { "name": "M1_37mm", @@ -12925,7 +12927,8 @@ "canTargetPoint": true, "canRearm": false, "markerFile": "groundunit-aaa", - "canAAA": true + "canAAA": true, + "aimTime": 5 }, "DR_50Ton_Flat_Wagon": { "name": "DR_50Ton_Flat_Wagon", @@ -13072,7 +13075,7 @@ "name": "Winter", "countries": "All" } - }, + }, "flak": true }, "flak36": { @@ -13101,7 +13104,7 @@ "name": "Winter", "countries": "All" } - }, + }, "flak": true }, "flak37": { @@ -13130,7 +13133,7 @@ "name": "Winter", "countries": "All" } - }, + }, "flak": true }, "flak38": { From 3aafa26c70656567fa7d1ed3c65429b857ff4d75 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Sun, 16 Mar 2025 22:33:28 +0100 Subject: [PATCH 17/33] feat: New predicting algorithm --- backend/core/src/groundunit.cpp | 24 +++++++++++++++++++++--- backend/core/src/scheduler.cpp | 5 +++++ frontend/react/src/ui/panels/header.tsx | 14 ++++++++++++++ frontend/react/src/weapon/weapon.ts | 8 +++++++- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/backend/core/src/groundunit.cpp b/backend/core/src/groundunit.cpp index 3923ce03..806db620 100644 --- a/backend/core/src/groundunit.cpp +++ b/backend/core/src/groundunit.cpp @@ -242,7 +242,12 @@ void GroundUnit::AIloop() if (!getHasTask()) { std::ostringstream taskSS; taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << ", radius = 100}"; + if (targetPosition.alt == NULL) { + taskSS << "{id = 'FireAtPoint', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << ", radius = 100}"; + } + else { + taskSS << "{id = 'FireAtPoint', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << ", alt = " << targetPosition.alt << ", radius = 100}"; + } Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); scheduler->appendCommand(command); setHasTask(true); @@ -429,9 +434,22 @@ void GroundUnit::AIloop() taskString += "Missing on purpose. Valid target at range: " + to_string((int) round(distance)) + "m"; double correctedAimTime = aimTime; + double dstep = 0; + double vstep = muzzleVelocity; + double dt = 0.1; + double k = 0.0086; + double gdelta = 9.81; + /* Approximate the flight time */ - if (muzzleVelocity != 0) - correctedAimTime += distance / muzzleVelocity; + unsigned int stepCount = 0; + if (muzzleVelocity != 0) { + while (dstep < distance && stepCount < 1000) { + dstep += vstep * dt; + vstep -= (k * vstep + gdelta) * dt; + stepCount++; + } + correctedAimTime += stepCount * dt; + } /* If the target is in targeting range and we are in highest precision mode, target it */ if (distance < targetingRange && shotsScatter == ShotsScatter::LOW) { diff --git a/backend/core/src/scheduler.cpp b/backend/core/src/scheduler.cpp index 30243911..663600d6 100644 --- a/backend/core/src/scheduler.cpp +++ b/backend/core/src/scheduler.cpp @@ -619,6 +619,11 @@ void Scheduler::handleRequest(string key, json::value value, string username, js double lat = value[L"location"][L"lat"].as_double(); double lng = value[L"location"][L"lng"].as_double(); Coords loc; loc.lat = lat; loc.lng = lng; + + if (value[L"location"].has_number_field(L"alt")) { + loc.alt = value[L"location"][L"alt"].as_double(); + } + Unit* unit = unitsManager->getGroupLeader(ID); if (unit != nullptr) { unit->setTargetPosition(loc); diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index e3891a3a..9da74fcf 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -50,6 +50,8 @@ import { import { OlympusConfig } from "../../interfaces"; import { FaCheck, FaQuestionCircle, FaSave, FaSpinner } from "react-icons/fa"; import { OlExpandingTooltip } from "../components/olexpandingtooltip"; +import { ftToM } from "../../other/utils"; +import { LatLng } from "leaflet"; export function Header() { const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS); @@ -219,6 +221,18 @@ export function Header() { />
+ { + getApp().getUnitsManager().getSelectedUnits().forEach((unit) => { + let position = new LatLng(unit.getPosition().lat, unit.getPosition().lng); + position.lat += 0.01; + position.alt = ftToM(15000); + unit.fireAtArea(position); + }) + }} + checked={false} + icon={faFlag} + />
{Object.entries({ human: olButtonsVisibilityHuman, diff --git a/frontend/react/src/weapon/weapon.ts b/frontend/react/src/weapon/weapon.ts index 0181c9e0..f692a84a 100644 --- a/frontend/react/src/weapon/weapon.ts +++ b/frontend/react/src/weapon/weapon.ts @@ -20,7 +20,9 @@ export abstract class Weapon extends CustomMarker { #hidden: boolean = false; #detectionMethods: number[] = []; - + #speedVector: number[] = []; + #altitude: number[] = []; + getAlive() { return this.#alive; } @@ -86,6 +88,7 @@ export abstract class Weapon extends CustomMarker { break; case DataIndexes.speed: this.#speed = dataExtractor.extractFloat64(); + this.#speedVector.push(this.#speed); updateMarker = true; break; case DataIndexes.heading: @@ -113,6 +116,9 @@ export abstract class Weapon extends CustomMarker { setAlive(newAlive: boolean) { this.#alive = newAlive; + if (this.#speedVector.length > 0 && newAlive === false) { + let asd = 1; + } } belongsToCommandedCoalition() { From c44caa9cea23628fc0fdcbda4d90e49e1e662bad Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Mon, 17 Mar 2025 16:54:33 +0100 Subject: [PATCH 18/33] feat: small change to flak mode miss on purpose --- backend/core/src/groundunit.cpp | 21 ++- databases/units/groundunitdatabase.json | 8 +- frontend/react/public/images/markers/flak.svg | 122 ++++++++++++++++++ frontend/react/src/map/map.ts | 7 + frontend/react/src/map/markers/flakmarker.ts | 39 ++++++ frontend/react/src/weapon/weapon.ts | 3 + 6 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 frontend/react/public/images/markers/flak.svg create mode 100644 frontend/react/src/map/markers/flakmarker.ts diff --git a/backend/core/src/groundunit.cpp b/backend/core/src/groundunit.cpp index 3923ce03..5aa11bd6 100644 --- a/backend/core/src/groundunit.cpp +++ b/backend/core/src/groundunit.cpp @@ -402,6 +402,14 @@ void GroundUnit::AIloop() canAAA = databaseEntry[L"canAAA"].as_bool(); } + /* Recover the data from the database */ + bool flak = false; + if (database.has_object_field(to_wstring(name))) { + json::value databaseEntry = database[to_wstring(name)]; + if (databaseEntry.has_boolean_field(L"flak")) + flak = databaseEntry[L"flak"].as_bool(); + } + if (canAAA) { /* Only perform scenic functions when the scheduler is "free" */ /* Only run this when the internal counter reaches 0 to avoid excessive computations when no nearby target */ @@ -450,13 +458,18 @@ void GroundUnit::AIloop() } /* Else, do miss on purpose */ else { - /* Compute where the target will be in aimTime seconds, plus the effect of scatter. */ - double scatterDistance = distance * tan(shotsBaseScatter * (ShotsScatter::LOW - shotsScatter) / 57.29577) * (RANDOM_ZERO_TO_ONE - 0.1); - double aimDistance = target->getHorizontalVelocity() * correctedAimTime + scatterDistance; + /* Compute where the target will be in aimTime seconds. */ + double aimDistance = target->getHorizontalVelocity() * correctedAimTime; double aimLat = 0; double aimLng = 0; Geodesic::WGS84().Direct(target->getPosition().lat, target->getPosition().lng, target->getTrack() * 57.29577, aimDistance, aimLat, aimLng); /* TODO make util to convert degrees and radians function */ - double aimAlt = target->getPosition().alt + target->getVerticalVelocity() * correctedAimTime + distance * tan(shotsBaseScatter * (ShotsScatter::LOW - shotsScatter) / 57.29577) * RANDOM_ZERO_TO_ONE; // Force to always miss high never low + double aimAlt = target->getPosition().alt + target->getVerticalVelocity(); + + if (flak) { + aimLat += RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 0.01; + aimLng += RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 0.01; + aimAlt += RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 1000; + } /* Send the command */ if (distance < engagementRange) { diff --git a/databases/units/groundunitdatabase.json b/databases/units/groundunitdatabase.json index 596feb34..90e54b8b 100644 --- a/databases/units/groundunitdatabase.json +++ b/databases/units/groundunitdatabase.json @@ -8625,8 +8625,8 @@ "abilities": "AA", "canTargetPoint": false, "canRearm": false, - "muzzleVelocity": 1000, - "aimTime": 25, + "muzzleVelocity": 700, + "aimTime": 50, "shotsToFire": 1, "barrelHeight": 5, "cost": null, @@ -10635,7 +10635,7 @@ "countries": "All" } }, - "aimTime": 15, + "aimTime": 50, "shotsToFire": 1, "acquisitionRange": 15000, "engagementRange": 12000, @@ -10643,7 +10643,7 @@ "abilities": "AA", "canTargetPoint": true, "canRearm": false, - "muzzleVelocity": 880, + "muzzleVelocity": 700, "barrelHeight": 2.1, "cost": 40000, "markerFile": "groundunit-aaa", diff --git a/frontend/react/public/images/markers/flak.svg b/frontend/react/public/images/markers/flak.svg new file mode 100644 index 00000000..146c0d87 --- /dev/null +++ b/frontend/react/public/images/markers/flak.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index bee24c70..4ec202ca 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -68,6 +68,7 @@ import { import { ContextActionSet } from "../unit/contextactionset"; import { SmokeMarker } from "./markers/smokemarker"; import { Measure } from "./measure"; +import { FlakMarker } from "./markers/flakmarker"; /* Register the handler for the box selection */ L.Map.addInitHook("addHandler", "boxSelect", BoxSelect); @@ -762,6 +763,12 @@ export class Map extends L.Map { return explosionMarker; } + addFlakMarker(latlng: L.LatLng) { + const explosionMarker = new FlakMarker(latlng, 10); + explosionMarker.addTo(this); + return explosionMarker; + } + addSmokeMarker(latlng: L.LatLng, color: string) { const smokeMarker = new SmokeMarker(latlng, color); smokeMarker.addTo(this); diff --git a/frontend/react/src/map/markers/flakmarker.ts b/frontend/react/src/map/markers/flakmarker.ts new file mode 100644 index 00000000..f2c581c1 --- /dev/null +++ b/frontend/react/src/map/markers/flakmarker.ts @@ -0,0 +1,39 @@ +import { CustomMarker } from "./custommarker"; +import { DivIcon, LatLng } from "leaflet"; +import { SVGInjector } from "@tanem/svg-injector"; +import { getApp } from "../../olympusapp"; + +export class FlakMarker extends CustomMarker { + #timer: number = 0; + #timeout: number = 0; + + constructor(latlng: LatLng, timeout?: number) { + super(latlng, { interactive: false }); + + if (timeout) { + this.#timeout = timeout; + + this.#timer = window.setTimeout(() => { + this.removeFrom(getApp().getMap()); + }, timeout * 1000); + } + } + + createIcon() { + /* Set the icon */ + this.setIcon( + new DivIcon({ + iconSize: [52, 52], + iconAnchor: [26, 26], + className: "leaflet-flak-marker", + }) + ); + var el = document.createElement("div"); + el.classList.add("ol-flak-icon"); + var img = document.createElement("img"); + img.src = "images/markers/flak.svg"; + img.onload = () => SVGInjector(img); + el.appendChild(img); + this.getElement()?.appendChild(el); + } +} diff --git a/frontend/react/src/weapon/weapon.ts b/frontend/react/src/weapon/weapon.ts index 0181c9e0..6b6f4b02 100644 --- a/frontend/react/src/weapon/weapon.ts +++ b/frontend/react/src/weapon/weapon.ts @@ -112,6 +112,9 @@ export abstract class Weapon extends CustomMarker { } setAlive(newAlive: boolean) { + if (this.#alive && !newAlive) { + getApp().getMap().addFlakMarker(this.getLatLng()); + } this.#alive = newAlive; } From dd2856a9933cc68f7cb8df495f306b918fa782fb Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 18 Mar 2025 10:58:35 +0100 Subject: [PATCH 19/33] fix: Laser and infrared correctly removed --- frontend/react/src/interfaces.ts | 2 +- frontend/react/src/mission/missionmanager.ts | 43 ++++++++++-------- frontend/react/src/mission/spot.ts | 46 ++++++++++++-------- frontend/react/src/unit/unit.ts | 32 +++++++++----- scripts/lua/backend/OlympusCommand.lua | 18 +++++--- 5 files changed, 86 insertions(+), 55 deletions(-) diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index f99e9f2f..869237cd 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -89,7 +89,7 @@ export interface BullseyesData { export interface SpotsData { spots: { - [key: string]: { type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number }; + [key: string]: { active: boolean; type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number }; }; sessionHash: string; time: number; diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index 2900dc06..d61247dd 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -6,13 +6,20 @@ import { BLUE_COMMANDER, GAME_MASTER, NONE, RED_COMMANDER } from "../constants/c import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData, SpotsData } from "../interfaces"; import { Coalition } from "../types/types"; import { Carrier } from "./carrier"; -import { AirbaseSelectedEvent, AppStateChangedEvent, BullseyesDataChangedEvent, CommandModeOptionsChangedEvent, EnabledCommandModesChangedEvent, MissionDataChangedEvent } from "../events"; +import { + AirbaseSelectedEvent, + AppStateChangedEvent, + BullseyesDataChangedEvent, + CommandModeOptionsChangedEvent, + EnabledCommandModesChangedEvent, + MissionDataChangedEvent, +} from "../events"; import { Spot } from "./spot"; /** The MissionManager */ export class MissionManager { #bullseyes: { [name: string]: Bullseye } = {}; - #spots: {[key: string]: Spot} = {}; + #spots: { [key: string]: Spot } = {}; #airbases: { [name: string]: Airbase | Carrier } = {}; #theatre: string = ""; #dateAndTime: DateAndTime = { @@ -39,7 +46,7 @@ export class MissionManager { constructor() { AppStateChangedEvent.on((state, subState) => { if (this.getSelectedAirbase() !== null) AirbaseSelectedEvent.dispatch(null); - }) + }); } /** Update location of bullseyes @@ -63,7 +70,7 @@ export class MissionManager { this.#bullseyes[idx].setCoalition(bullseye.coalition); } - BullseyesDataChangedEvent.dispatch(this.#bullseyes) + BullseyesDataChangedEvent.dispatch(this.#bullseyes); } } @@ -72,18 +79,18 @@ export class MissionManager { const spotID = Number(idx); const spot = data.spots[idx]; if (this.#spots[spotID] === undefined) { - this.#spots[spotID] = new Spot(spotID, spot.type, new LatLng(spot.targetPosition.lat, spot.targetPosition.lng), spot.sourceUnitID, spot.code); + this.#spots[spotID] = new Spot( + spotID, + spot.type, + new LatLng(spot.targetPosition.lat, spot.targetPosition.lng), + spot.sourceUnitID, + spot.active, + spot.code + ); } else { - if (spot.type === "laser") - this.#spots[spotID].setCode(spot.code ?? 0) - this.#spots[spotID].setTargetPosition( new LatLng(spot.targetPosition.lat, spot.targetPosition.lng)); - } - } - - /* Iterate the existing spots and remove all spots that where deleted */ - for (let idx in this.#spots) { - if (data.spots[idx] === undefined) { - delete this.#spots[idx]; + if (spot.type === "laser") this.#spots[spotID].setCode(spot.code ?? 0); + this.#spots[spotID].setActive(spot.active); + this.#spots[spotID].setTargetPosition(new LatLng(spot.targetPosition.lat, spot.targetPosition.lng)); } } } @@ -99,7 +106,7 @@ export class MissionManager { updateAirbases(data: AirbasesData) { for (let idx in data.airbases) { var airbase = data.airbases[idx]; - var airbaseCallsign = airbase.callsign !== ""? airbase.callsign: `carrier-${airbase.unitId}` + var airbaseCallsign = airbase.callsign !== "" ? airbase.callsign : `carrier-${airbase.unitId}`; if (this.#airbases[airbaseCallsign] === undefined) { if (airbase.callsign != "") { this.#airbases[airbaseCallsign] = new Airbase({ @@ -161,7 +168,7 @@ export class MissionManager { return this.#airbases; } - getSpots() { + getSpots() { return this.#spots; } @@ -279,7 +286,7 @@ export class MissionManager { commandModeOptions.spawnPoints.red !== this.getCommandModeOptions().spawnPoints.red || commandModeOptions.spawnPoints.blue !== this.getCommandModeOptions().spawnPoints.blue || commandModeOptions.restrictSpawns !== this.getCommandModeOptions().restrictSpawns || - commandModeOptions.restrictToCoalition !== this.getCommandModeOptions().restrictToCoalition || + commandModeOptions.restrictToCoalition !== this.getCommandModeOptions().restrictToCoalition || commandModeOptions.setupTime !== this.getCommandModeOptions().setupTime; this.#commandModeOptions = commandModeOptions; diff --git a/frontend/react/src/mission/spot.ts b/frontend/react/src/mission/spot.ts index 2bd72624..95f10735 100644 --- a/frontend/react/src/mission/spot.ts +++ b/frontend/react/src/mission/spot.ts @@ -2,47 +2,57 @@ import { LatLng } from "leaflet"; import { getApp } from "../olympusapp"; export class Spot { - private ID: number; - private type: string; - private targetPosition: LatLng; - private sourceUnitID: number; - private code?: number; + #ID: number; + #type: string; + #targetPosition: LatLng; + #sourceUnitID: number; + #active: boolean; + #code?: number; - constructor(ID: number, type: string, targetPosition: LatLng, sourceUnitID: number, code?: number) { - this.ID = ID; - this.type = type; - this.targetPosition = targetPosition; - this.sourceUnitID = sourceUnitID; - this.code = code; + constructor(ID: number, type: string, targetPosition: LatLng, sourceUnitID: number, active: boolean, code?: number) { + this.#ID = ID; + this.#type = type; + this.#targetPosition = targetPosition; + this.#sourceUnitID = sourceUnitID; + this.#code = code; + this.#active = active; } // Getter methods getID() { - return this.ID; + return this.#ID; } getType() { - return this.type; + return this.#type; } getTargetPosition() { - return this.targetPosition; + return this.#targetPosition; } getSourceUnitID() { - return this.sourceUnitID; + return this.#sourceUnitID; } getCode() { - return this.code; + return this.#code; + } + + getActive() { + return this.#active; } // Setter methods setTargetPosition(position: LatLng) { - this.targetPosition = position; + this.#targetPosition = position; } setCode(code: number) { - this.code = code; + this.#code = code; + } + + setActive(active: boolean) { + this.#active = active; } } \ No newline at end of file diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 917e33bc..ed44267d 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -1953,21 +1953,29 @@ export abstract class Unit extends CustomMarker { // Iterate over all spots and draw lines, edit markers, and markers Object.values(getApp().getMissionManager().getSpots()).forEach((spot: Spot) => { if (spot.getSourceUnitID() === this.ID) { - const spotBearing = deg2rad(bearing(this.getPosition().lat, this.getPosition().lng, spot.getTargetPosition().lat, spot.getTargetPosition().lng, false)); - const spotDistance = this.getPosition().distanceTo(spot.getTargetPosition()); - const midPosition = bearingAndDistanceToLatLng(this.getPosition().lat, this.getPosition().lng, spotBearing, spotDistance / 2); + if (spot.getActive()) { + const spotBearing = deg2rad( + bearing(this.getPosition().lat, this.getPosition().lng, spot.getTargetPosition().lat, spot.getTargetPosition().lng, false) + ); + const spotDistance = this.getPosition().distanceTo(spot.getTargetPosition()); + const midPosition = bearingAndDistanceToLatLng(this.getPosition().lat, this.getPosition().lng, spotBearing, spotDistance / 2); - // Draw the spot line - this.#drawSpotLine(spot, spotBearing); + // Draw the spot line + this.#drawSpotLine(spot, spotBearing); - // Draw the spot edit marker if the map is zoomed in enough - if (getApp().getMap().getZoom() >= SPOTS_EDIT_ZOOM_TRANSITION) { - // Draw the spot edit marker - this.#drawSpotEditMarker(spot, midPosition, spotBearing); + // Draw the spot edit marker if the map is zoomed in enough + if (getApp().getMap().getZoom() >= SPOTS_EDIT_ZOOM_TRANSITION) { + // Draw the spot edit marker + this.#drawSpotEditMarker(spot, midPosition, spotBearing); + } + + // Draw the spot marker + this.#drawSpotMarker(spot); + } else { + this.#spotLines[spot.getID()]?.removeFrom(getApp().getMap()); + this.#spotEditMarkers[spot.getID()]?.removeFrom(getApp().getMap()); + this.#spotMarkers[spot.getID()]?.removeFrom(getApp().getMap()); } - - // Draw the spot marker - this.#drawSpotMarker(spot); } }); } diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua index 4a0d2385..44f27268 100644 --- a/scripts/lua/backend/OlympusCommand.lua +++ b/scripts/lua/backend/OlympusCommand.lua @@ -589,6 +589,7 @@ function Olympus.fireLaser(ID, code, lat, lng) lat = lat, lng = lng }, + active = true, code = code } end @@ -611,13 +612,15 @@ function Olympus.fireInfrared(ID, lat, lng) targetPosition = { lat = lat, lng = lng - } + }, + active = true } end end -- Set new laser code function Olympus.setLaserCode(spotID, code) + Olympus.debug("Olympus.setLaserCode " .. spotID .. " -> " .. code, 2) local spot = Olympus.spots[spotID] if spot ~= nil and spot.type == "laser" then spot.object:setCode(code) @@ -627,19 +630,21 @@ end -- Move spot to a new location function Olympus.moveSpot(spotID, lat, lng) + Olympus.debug("Olympus.moveSpot " .. spotID .. " -> (" .. lat .. ", " .. lng .. ")", 2) local spot = Olympus.spots[spotID] if spot ~= nil then - spot.object:setPoint(coord.LLtoLO(lat, lng, 0)) + spot.object:setPoint(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0))) spot.targetPosition = {lat = lat, lng = lng} end end -- Remove the spot function Olympus.deleteSpot(spotID) + Olympus.debug("Olympus.deleteSpot " .. spotID, 2) local spot = Olympus.spots[spotID] if spot ~= nil then spot.object:destroy() - Olympus.spots[spotID] = nil + Olympus.spots[spotID]["active"] = false end end @@ -1415,8 +1420,8 @@ function Olympus.setWeaponsData(arg, time) table["category"] = "Missile" elseif weapon:getDesc().category == Weapon.Category.BOMB then table["category"] = "Bomb" - elseif weapon:getDesc().category == Weapon.Category.SHELL then - table["category"] = "Shell" + --elseif weapon:getDesc().category == Weapon.Category.SHELL then + -- table["category"] = "Shell" -- Useful for debugging but has no real use and has big impact on performance end else weapons[ID] = {isAlive = false} @@ -1527,6 +1532,7 @@ function Olympus.setMissionData(arg, time) type = spot.type, sourceUnitID = spot.sourceUnitID, targetPosition = spot.targetPosition, + active = spot.active, } -- If the spot type is "laser", add the code to the spot entry @@ -1542,7 +1548,7 @@ function Olympus.setMissionData(arg, time) Olympus.missionData["spots"] = spots Olympus.OlympusDLL.setMissionData() - return time + 1 -- For perfomance reasons weapons are updated once every second + return time + 1 -- For perfomance reasons mission data is updated once every second end -- Initializes the units table with all the existing ME units From ddf9883f892b20cae80145b05cb1ef74d09d88e3 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 18 Mar 2025 11:05:33 +0100 Subject: [PATCH 20/33] fix: Reverted some changes to miss on purpose mode --- backend/core/src/groundunit.cpp | 31 +++++++------------------ frontend/react/src/ui/panels/header.tsx | 12 ---------- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/backend/core/src/groundunit.cpp b/backend/core/src/groundunit.cpp index e15bcb15..ab0ccead 100644 --- a/backend/core/src/groundunit.cpp +++ b/backend/core/src/groundunit.cpp @@ -357,8 +357,8 @@ void GroundUnit::AIloop() Geodesic::WGS84().Direct(position.lat, position.lng, randomBearing, r, lat, lng); if (flak) { - lat = position.lat + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 0.01; - lng = position.lng + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 0.01; + lat = position.lat + RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; + lng = position.lng + RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; barrelElevation = target->getPosition().alt + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 1000; taskString += "Flak box mode."; } @@ -441,24 +441,9 @@ void GroundUnit::AIloop() if (target != nullptr) { taskString += "Missing on purpose. Valid target at range: " + to_string((int) round(distance)) + "m"; - double correctedAimTime = aimTime; - double dstep = 0; - double vstep = muzzleVelocity; - double dt = 0.1; - double k = 0.0086; - double gdelta = 9.81; - - /* Approximate the flight time */ - unsigned int stepCount = 0; - if (muzzleVelocity != 0) { - while (dstep < distance && stepCount < 1000) { - dstep += vstep * dt; - vstep -= (k * vstep + gdelta) * dt; - stepCount++; - } - correctedAimTime += stepCount * dt; - } - + // Very simplified algorithm ignoring drag + double correctedAimTime = aimTime + distance / muzzleVelocity; + /* If the target is in targeting range and we are in highest precision mode, target it */ if (distance < targetingRange && shotsScatter == ShotsScatter::LOW) { taskString += ". Range is less than targeting range (" + to_string((int) round(targetingRange)) + "m) and scatter is LOW, aiming at target."; @@ -484,9 +469,9 @@ void GroundUnit::AIloop() double aimAlt = target->getPosition().alt + target->getVerticalVelocity(); if (flak) { - aimLat += RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 0.01; - aimLng += RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 0.01; - aimAlt += RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 1000; + aimLat += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; + aimLng += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; + aimAlt += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 1000; } /* Send the command */ diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 9da74fcf..8429e62b 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -221,18 +221,6 @@ export function Header() { />
- { - getApp().getUnitsManager().getSelectedUnits().forEach((unit) => { - let position = new LatLng(unit.getPosition().lat, unit.getPosition().lng); - position.lat += 0.01; - position.alt = ftToM(15000); - unit.fireAtArea(position); - }) - }} - checked={false} - icon={faFlag} - />
{Object.entries({ human: olButtonsVisibilityHuman, From cf86c4ade9d0ee8d6963a28644720b846298fa2d Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 18 Mar 2025 12:53:22 +0100 Subject: [PATCH 21/33] fix: minor improvements to miss on purpose mode --- backend/core/src/groundunit.cpp | 443 +++++++++++++----------- databases/units/groundunitdatabase.json | 18 +- 2 files changed, 242 insertions(+), 219 deletions(-) diff --git a/backend/core/src/groundunit.cpp b/backend/core/src/groundunit.cpp index ab0ccead..74d6272d 100644 --- a/backend/core/src/groundunit.cpp +++ b/backend/core/src/groundunit.cpp @@ -108,12 +108,14 @@ void GroundUnit::setState(unsigned char newState) /************ Perform any action required when ENTERING a state ************/ switch (newState) { case State::IDLE: { + setTask("Idle"); setEnableTaskCheckFailed(false); clearActivePath(); resetActiveDestination(); break; } case State::REACH_DESTINATION: { + setTask("Reaching destination"); setEnableTaskCheckFailed(true); resetActiveDestination(); break; @@ -125,6 +127,7 @@ void GroundUnit::setState(unsigned char newState) break; } case State::FIRE_AT_AREA: { + setTask("Firing at area"); setTargetPosition(currentTargetPosition); setEnableTaskCheckFailed(true); clearActivePath(); @@ -132,6 +135,7 @@ void GroundUnit::setState(unsigned char newState) break; } case State::SIMULATE_FIRE_FIGHT: { + setTask("Simulating fire fight"); setTargetPosition(currentTargetPosition); setEnableTaskCheckFailed(false); clearActivePath(); @@ -139,12 +143,14 @@ void GroundUnit::setState(unsigned char newState) break; } case State::SCENIC_AAA: { + setTask("Scenic AAA"); setEnableTaskCheckFailed(false); clearActivePath(); resetActiveDestination(); break; } case State::MISS_ON_PURPOSE: { + setTask("Miss on purpose"); setEnableTaskCheckFailed(false); clearActivePath(); resetActiveDestination(); @@ -156,6 +162,7 @@ void GroundUnit::setState(unsigned char newState) setHasTask(false); resetTaskFailedCounter(); + nextTaskingMilliseconds = 0; log(unitName + " setting state from " + to_string(state) + " to " + to_string(newState)); state = newState; @@ -183,14 +190,12 @@ void GroundUnit::AIloop() switch (state) { case State::IDLE: { - setTask("Idle"); if (getHasTask()) resetTask(); + break; } case State::REACH_DESTINATION: { - setTask("Reaching destination"); - string enrouteTask = ""; bool looping = false; @@ -237,8 +242,6 @@ void GroundUnit::AIloop() break; } case State::FIRE_AT_AREA: { - setTask("Firing at area"); - if (!getHasTask()) { std::ostringstream taskSS; taskSS.precision(10); @@ -258,55 +261,59 @@ void GroundUnit::AIloop() case State::SIMULATE_FIRE_FIGHT: { string taskString = ""; - if ( - (totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) && - targetPosition != Coords(NULL) && - scheduler->getLoad() < 30 - ) { - /* Get the distance and bearing to the target */ - Coords scatteredTargetPosition = targetPosition; - double distance; - double bearing1; - double bearing2; - Geodesic::WGS84().Inverse(getPosition().lat, getPosition().lng, scatteredTargetPosition.lat, scatteredTargetPosition.lng, distance, bearing1, bearing2); - - /* Apply a scatter to the aim */ - bearing1 += RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter + 1) * 10; - - /* Compute the scattered position applying a random scatter to the shot */ - double scatterDistance = distance * tan(10 /* degs */ * (ShotsScatter::LOW - shotsScatter) / 57.29577 + 2 / 57.29577 /* degs */) * RANDOM_MINUS_ONE_TO_ONE; - Geodesic::WGS84().Direct(scatteredTargetPosition.lat, scatteredTargetPosition.lng, bearing1, scatterDistance, scatteredTargetPosition.lat, scatteredTargetPosition.lng); - - /* Recover the data from the database */ - bool indirectFire = false; - if (database.has_object_field(to_wstring(name))) { - json::value databaseEntry = database[to_wstring(name)]; - if (databaseEntry.has_boolean_field(L"indirectFire")) - indirectFire = databaseEntry[L"indirectFire"].as_bool(); + if ((totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) && targetPosition != Coords(NULL)) { + if (scheduler->getLoad() > 100) { + taskString = "Excessive load, skipping tasking of unit"; + setTargetPosition(Coords(NULL)); + if (getHasTask()) + resetTask(); } - - /* If the unit is of the indirect fire type, like a mortar, simply shoot at the target */ - if (indirectFire) { - taskString += "Simulating fire fight with indirect fire"; - log(unitName + "(" + name + ")" + " simulating fire fight with indirect fire"); - std::ostringstream taskSS; - taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << scatteredTargetPosition.lat << ", lng = " << scatteredTargetPosition.lng << ", radius = 0.01}"; - Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); - scheduler->appendCommand(command); - shellsFiredAtTasking = totalShellsFired; - setHasTask(true); - } - /* Otherwise use the aim method */ else { - taskString += "Simulating fire fight with aim point method. "; - log(unitName + "(" + name + ")" + " simulating fire fight with aim at point method"); - string aimTaskString = aimAtPoint(scatteredTargetPosition); - taskString += aimTaskString; - } + /* Get the distance and bearing to the target */ + Coords scatteredTargetPosition = targetPosition; + double distance; + double bearing1; + double bearing2; + Geodesic::WGS84().Inverse(getPosition().lat, getPosition().lng, scatteredTargetPosition.lat, scatteredTargetPosition.lng, distance, bearing1, bearing2); - /* Wait an amout of time depending on the shots intensity */ - nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); + /* Apply a scatter to the aim */ + bearing1 += RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter + 1) * 10; + + /* Compute the scattered position applying a random scatter to the shot */ + double scatterDistance = distance * tan(10 /* degs */ * (ShotsScatter::LOW - shotsScatter) / 57.29577 + 2 / 57.29577 /* degs */) * RANDOM_MINUS_ONE_TO_ONE; + Geodesic::WGS84().Direct(scatteredTargetPosition.lat, scatteredTargetPosition.lng, bearing1, scatterDistance, scatteredTargetPosition.lat, scatteredTargetPosition.lng); + + /* Recover the data from the database */ + bool indirectFire = false; + if (database.has_object_field(to_wstring(name))) { + json::value databaseEntry = database[to_wstring(name)]; + if (databaseEntry.has_boolean_field(L"indirectFire")) + indirectFire = databaseEntry[L"indirectFire"].as_bool(); + } + + /* If the unit is of the indirect fire type, like a mortar, simply shoot at the target */ + if (indirectFire) { + taskString += "Simulating fire fight with indirect fire"; + log(unitName + "(" + name + ")" + " simulating fire fight with indirect fire"); + std::ostringstream taskSS; + taskSS.precision(10); + taskSS << "{id = 'FireAtPoint', lat = " << scatteredTargetPosition.lat << ", lng = " << scatteredTargetPosition.lng << ", radius = 0.01}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); + scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; + setHasTask(true); + } + /* Otherwise use the aim method */ + else { + taskString += "Simulating fire fight with aim point method. "; + log(unitName + "(" + name + ")" + " simulating fire fight with aim at point method"); + string aimTaskString = aimAtPoint(scatteredTargetPosition); + taskString += aimTaskString; + } + + /* Wait an amout of time depending on the shots intensity */ + nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); + } } if (targetPosition == Coords(NULL)) @@ -327,63 +334,71 @@ void GroundUnit::AIloop() string taskString = ""; /* Only perform scenic functions when the scheduler is "free" */ - if ((totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) && - scheduler->getLoad() < 30) { - double distance = 0; - unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition; - unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2; - Unit* target = unitsManager->getClosestUnit(this, targetCoalition, { "Aircraft", "Helicopter" }, distance); - - /* Recover the data from the database */ - bool flak = false; - if (database.has_object_field(to_wstring(name))) { - json::value databaseEntry = database[to_wstring(name)]; - if (databaseEntry.has_boolean_field(L"flak")) - flak = databaseEntry[L"flak"].as_bool(); - } - - /* Only run if an enemy air unit is closer than 20km to avoid useless load */ - double activationDistance = 20000; - if (2 * engagementRange > activationDistance) - activationDistance = 2 * engagementRange; - - if (target != nullptr && distance < activationDistance /* m */) { - double r = 15; /* m */ - double barrelElevation = position.alt + barrelHeight + r * tan(acos(((double)(rand()) / (double)(RAND_MAX)))); - - double lat = 0; - double lng = 0; - double randomBearing = ((double)(rand()) / (double)(RAND_MAX)) * 360; - Geodesic::WGS84().Direct(position.lat, position.lng, randomBearing, r, lat, lng); - - if (flak) { - lat = position.lat + RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; - lng = position.lng + RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; - barrelElevation = target->getPosition().alt + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 1000; - taskString += "Flak box mode."; - } - else { - taskString += "Scenic AAA. Bearing: " + to_string((int)round(randomBearing)) + "deg"; - } - - taskString += ". Aim point elevation " + to_string((int) round(barrelElevation - position.alt)) + "m AGL"; - - std::ostringstream taskSS; - taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << barrelElevation << ", radius = 0.001 }"; - Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); - scheduler->appendCommand(command); - shellsFiredAtTasking = totalShellsFired; - setHasTask(true); - - /* Wait an amout of time depending on the shots intensity */ - nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); + if (totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) { + if (scheduler->getLoad() > 100) { + taskString = "Excessive load, skipping tasking of unit"; + setTargetPosition(Coords(NULL)); + if (getHasTask()) + resetTask(); } else { - if (target == nullptr) - taskString += "Scenic AAA. No valid target."; - else - taskString += "Scenic AAA. Target outside max range: " + to_string((int)round(distance)) + "m."; + double distance = 0; + unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition; + unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2; + Unit* target = unitsManager->getClosestUnit(this, targetCoalition, { "Aircraft", "Helicopter" }, distance); + + /* Recover the data from the database */ + bool flak = false; + if (database.has_object_field(to_wstring(name))) { + json::value databaseEntry = database[to_wstring(name)]; + if (databaseEntry.has_boolean_field(L"flak")) + flak = databaseEntry[L"flak"].as_bool(); + } + + /* Only run if an enemy air unit is closer than 20km to avoid useless load */ + double activationDistance = 20000; + if (2 * engagementRange > activationDistance) + activationDistance = 2 * engagementRange; + + if (target != nullptr && distance < activationDistance /* m */) { + double r = 15; /* m */ + double barrelElevation = position.alt + barrelHeight + r * tan(acos(((double)(rand()) / (double)(RAND_MAX)))); + + double lat = 0; + double lng = 0; + double randomBearing = ((double)(rand()) / (double)(RAND_MAX)) * 360; + Geodesic::WGS84().Direct(position.lat, position.lng, randomBearing, r, lat, lng); + + if (flak) { + lat = position.lat + RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; + lng = position.lng + RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; + barrelElevation = target->getPosition().alt + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 1000; + taskString += "Flak box mode."; + } + else { + taskString += "Scenic AAA. Bearing: " + to_string((int)round(randomBearing)) + "deg"; + } + + taskString += ". Aim point elevation " + to_string((int)round(barrelElevation - position.alt)) + "m AGL"; + + std::ostringstream taskSS; + taskSS.precision(10); + taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << barrelElevation << ", radius = 0.001 }"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); + scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; + setHasTask(true); + + /* Wait an amout of time depending on the shots intensity */ + nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); + } + else { + setTargetPosition(Coords(NULL)); + if (target == nullptr) + taskString += "Scenic AAA. No valid target."; + else + taskString += "Scenic AAA. Target outside max range: " + to_string((int)round(distance)) + "m."; + } } } @@ -418,113 +433,121 @@ void GroundUnit::AIloop() if (canAAA) { /* Only perform scenic functions when the scheduler is "free" */ /* Only run this when the internal counter reaches 0 to avoid excessive computations when no nearby target */ - if ((totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) && - scheduler->getLoad() < 30) { - double distance = 0; - unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition; - unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2; - - /* Get all the units in range and select one at random */ - double range = max(max(engagementRange, aimMethodRange), acquisitionRange); - map targets = unitsManager->getUnitsInRange(this, targetCoalition, { "Aircraft", "Helicopter" }, range); - - Unit* target = nullptr; - unsigned int index = static_cast((RANDOM_ZERO_TO_ONE * (targets.size() - 1))); - for (auto const& p : targets) { - if (index-- == 0) { - target = p.first; - distance = p.second; - } - } - - /* Only do if we have a valid target close enough for AAA */ - if (target != nullptr) { - taskString += "Missing on purpose. Valid target at range: " + to_string((int) round(distance)) + "m"; - - // Very simplified algorithm ignoring drag - double correctedAimTime = aimTime + distance / muzzleVelocity; - - /* If the target is in targeting range and we are in highest precision mode, target it */ - if (distance < targetingRange && shotsScatter == ShotsScatter::LOW) { - taskString += ". Range is less than targeting range (" + to_string((int) round(targetingRange)) + "m) and scatter is LOW, aiming at target."; - - /* Send the command */ - std::ostringstream taskSS; - taskSS.precision(10); - taskSS << "{id = 'AttackUnit', unitID = " << target->getID() << " }"; - Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); - scheduler->appendCommand(command); - shellsFiredAtTasking = totalShellsFired; - setHasTask(true); - - nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); - } - /* Else, do miss on purpose */ - else { - /* Compute where the target will be in aimTime seconds. */ - double aimDistance = target->getHorizontalVelocity() * correctedAimTime; - double aimLat = 0; - double aimLng = 0; - Geodesic::WGS84().Direct(target->getPosition().lat, target->getPosition().lng, target->getTrack() * 57.29577, aimDistance, aimLat, aimLng); /* TODO make util to convert degrees and radians function */ - double aimAlt = target->getPosition().alt + target->getVerticalVelocity(); - - if (flak) { - aimLat += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; - aimLng += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; - aimAlt += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 1000; - } - - /* Send the command */ - if (distance < engagementRange) { - taskString += ". Range is less than engagement range (" + to_string((int) round(engagementRange)) + "m), using FIRE AT POINT method"; - - /* If the unit is closer than the engagement range, use the fire at point method */ - std::ostringstream taskSS; - taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << aimLat << ", lng = " << aimLng << ", alt = " << aimAlt << ", radius = 0.001 }"; - - taskString += ". Aiming altitude " + to_string((int)round((aimAlt - position.alt) / 0.3048)) + "ft AGL"; - Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); - scheduler->appendCommand(command); - shellsFiredAtTasking = totalShellsFired; - setHasTask(true); - setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); - nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); - } - else if (distance < aimMethodRange) { - taskString += ". Range is less than aim method range (" + to_string((int)round(aimMethodRange / 0.3048)) + "ft), using AIM method."; - - /* If the unit is closer than the aim method range, use the aim method range */ - string aimMethodTask = aimAtPoint(Coords(aimLat, aimLng, aimAlt)); - taskString += aimMethodTask; - - setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); - nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); - } - else { - taskString += ". Target is not in range of weapon, waking up unit to get ready for tasking."; - - /* Else just wake the unit up with an impossible command */ - std::ostringstream taskSS; - taskSS.precision(10); - taskSS << "{id = 'FireAtPoint', lat = " << 0 << ", lng = " << 0 << ", alt = " << 0 << ", radius = 0.001, expendQty = " << 0 << " }"; - Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); - scheduler->appendCommand(command); - shellsFiredAtTasking = totalShellsFired; - setHasTask(true); - setTargetPosition(Coords(NULL)); - - /* Don't wait too long before checking again */ - nextTaskingMilliseconds = timeNow + static_cast(5 * 1000); - } - } - missOnPurposeTarget = target; - } - else { - taskString += "Missing on purpose. No target in range."; + if (totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) { + if (scheduler->getLoad() > 100) { + taskString = "Excessive load, skipping tasking of unit"; + setTargetPosition(Coords(NULL)); if (getHasTask()) resetTask(); } + else { + double distance = 0; + unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition; + unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2; + + /* Get all the units in range and select one at random */ + double range = max(max(engagementRange, aimMethodRange), acquisitionRange); + map targets = unitsManager->getUnitsInRange(this, targetCoalition, { "Aircraft", "Helicopter" }, range); + + Unit* target = nullptr; + unsigned int index = static_cast((RANDOM_ZERO_TO_ONE * (targets.size() - 1))); + for (auto const& p : targets) { + if (index-- == 0) { + target = p.first; + distance = p.second; + } + } + + /* Only do if we have a valid target close enough for AAA */ + if (target != nullptr) { + taskString += "Missing on purpose. Valid target at range: " + to_string((int)round(distance)) + "m"; + + // Very simplified algorithm ignoring drag + double correctedAimTime = aimTime + distance / muzzleVelocity; + + /* If the target is in targeting range and we are in highest precision mode, target it */ + if (distance < targetingRange && shotsScatter == ShotsScatter::LOW) { + taskString += ". Range is less than targeting range (" + to_string((int)round(targetingRange)) + "m) and scatter is LOW, aiming at target."; + + /* Send the command */ + std::ostringstream taskSS; + taskSS.precision(10); + taskSS << "{id = 'AttackUnit', unitID = " << target->getID() << " }"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); + scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; + setHasTask(true); + + nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); + } + /* Else, do miss on purpose */ + else { + /* Compute where the target will be in aimTime seconds. */ + double aimDistance = target->getHorizontalVelocity() * correctedAimTime; + double aimLat = 0; + double aimLng = 0; + Geodesic::WGS84().Direct(target->getPosition().lat, target->getPosition().lng, target->getTrack() * 57.29577, aimDistance, aimLat, aimLng); /* TODO make util to convert degrees and radians function */ + double aimAlt = target->getPosition().alt + target->getVerticalVelocity(); + + if (flak) { + aimLat += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; + aimLng += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01; + aimAlt += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 1000; + } + + /* Send the command */ + if (distance < engagementRange) { + taskString += ". Range is less than engagement range (" + to_string((int)round(engagementRange)) + "m), using FIRE AT POINT method"; + + /* If the unit is closer than the engagement range, use the fire at point method */ + std::ostringstream taskSS; + taskSS.precision(10); + taskSS << "{id = 'FireAtPoint', lat = " << aimLat << ", lng = " << aimLng << ", alt = " << aimAlt << ", radius = 0.001 }"; + + taskString += ". Aiming altitude " + to_string((int)round((aimAlt - position.alt) / 0.3048)) + "ft AGL"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); + scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; + setHasTask(true); + setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); + nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); + } + else if (distance < aimMethodRange) { + taskString += ". Range is less than aim method range (" + to_string((int)round(aimMethodRange / 0.3048)) + "ft), using AIM method."; + + /* If the unit is closer than the aim method range, use the aim method range */ + string aimMethodTask = aimAtPoint(Coords(aimLat, aimLng, aimAlt)); + taskString += aimMethodTask; + + setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); + nextTaskingMilliseconds = timeNow + static_cast(2 * aimTime * 1000); + } + else { + taskString += ". Target is not in range of weapon, waking up unit to get ready for tasking."; + + /* Else just wake the unit up with an impossible command */ + std::ostringstream taskSS; + taskSS.precision(10); + taskSS << "{id = 'FireAtPoint', lat = " << 0 << ", lng = " << 0 << ", alt = " << 0 << ", radius = 0.001, expendQty = " << 0 << " }"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); + scheduler->appendCommand(command); + shellsFiredAtTasking = totalShellsFired; + setHasTask(true); + setTargetPosition(Coords(NULL)); + + /* Don't wait too long before checking again */ + nextTaskingMilliseconds = timeNow + static_cast(5 * 1000); + } + } + missOnPurposeTarget = target; + } + else { + taskString += "Missing on purpose. No target in range."; + setTargetPosition(Coords(NULL)); + if (getHasTask()) + resetTask(); + } + } } /* If no valid target was detected */ diff --git a/databases/units/groundunitdatabase.json b/databases/units/groundunitdatabase.json index 90e54b8b..23a6a6c1 100644 --- a/databases/units/groundunitdatabase.json +++ b/databases/units/groundunitdatabase.json @@ -8625,9 +8625,9 @@ "abilities": "AA", "canTargetPoint": false, "canRearm": false, - "muzzleVelocity": 700, + "muzzleVelocity": 600, "aimTime": 50, - "shotsToFire": 1, + "shotsToFire": 10, "barrelHeight": 5, "cost": null, "markerFile": "groundunit-aaa", @@ -8635,7 +8635,7 @@ "targetingRange": 100, "aimMethodRange": 100, "shotsBaseInterval": 5, - "shotsBaseScatter": 5, + "shotsBaseScatter": 15, "alertnessTimeConstant": 5, "flak": true }, @@ -9514,20 +9514,20 @@ } }, "acquisitionRange": 10000, - "engagementRange": 3000, + "engagementRange": 9000, "description": "ZSU-57-2. Tracked self propelled optically guided AA gun. 2 x 57 mm auto cannon.", "abilities": "Combined arms, AA", "canTargetPoint": true, "canRearm": false, - "muzzleVelocity": 1200, + "muzzleVelocity": 1000, "barrelHeight": 3, - "aimTime": 10, + "aimTime": 15, "shotsToFire": 5, "cost": null, "tags": "CA", "markerFile": "groundunit-aaa", "canAAA": true, - "aimMethodRange": 9000, + "aimMethodRange": 100, "targetingRange": 100, "shotsBaseInterval": 5, "shotsBaseScatter": 5, @@ -10636,14 +10636,14 @@ } }, "aimTime": 50, - "shotsToFire": 1, + "shotsToFire": 5, "acquisitionRange": 15000, "engagementRange": 12000, "description": "The flak 88. Fixed anti aircraft gun famously also used as an anti-tank gun. 88mm flak gun.", "abilities": "AA", "canTargetPoint": true, "canRearm": false, - "muzzleVelocity": 700, + "muzzleVelocity": 880, "barrelHeight": 2.1, "cost": 40000, "markerFile": "groundunit-aaa", From 50f3882b3ea0c38a9eecc6852aae191bd9d6d87f Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 18 Mar 2025 13:02:12 +0100 Subject: [PATCH 22/33] fix: "Alt" keybinds getting stuck when switching windows with alt+tab --- frontend/react/src/shortcut/shortcut.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/react/src/shortcut/shortcut.ts b/frontend/react/src/shortcut/shortcut.ts index 39b0d951..3949286e 100644 --- a/frontend/react/src/shortcut/shortcut.ts +++ b/frontend/react/src/shortcut/shortcut.ts @@ -21,7 +21,7 @@ export class Shortcut { ModalEvent.on((modal) => (this.#modal = modal)); /* On keyup, it is enough to check the code only, not the entire combination */ - document.addEventListener("keyup", (ev: any) => { + window.addEventListener("keyup", (ev: any) => { if (this.#modal) return; if (this.#keydown && this.getOptions().code === ev.code) { console.log(`Keyup for shortcut ${this.#id}`); @@ -32,7 +32,7 @@ export class Shortcut { }); /* Forced keyup, in case the window loses focus */ - document.addEventListener("blur", (ev: any) => { + window.addEventListener("blur", (ev: any) => { if (this.#keydown) { console.log(`Keyup (forced by blur) for shortcut ${this.#id}`); ev.preventDefault(); @@ -42,7 +42,7 @@ export class Shortcut { }); /* On keydown, check exactly if the requested key combination is being pressed */ - document.addEventListener("keydown", (ev: any) => { + window.addEventListener("keydown", (ev: any) => { if (this.#modal) return; if ( !(this.#keydown || keyEventWasInInput(ev) || this.getOptions().code !== ev.code) && From 3c33d3883efacc6da99f1b6458f2ad9482659bd3 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 18 Mar 2025 16:14:39 +0100 Subject: [PATCH 23/33] fix: Small fixes to responsive design --- frontend/react/src/map/boxselect.ts | 6 +- frontend/react/src/map/map.ts | 11 ++- .../react/src/ui/components/ollocation.tsx | 27 +++++- .../react/src/ui/components/oltooltip.tsx | 8 +- frontend/react/src/ui/panels/airbasemenu.tsx | 2 +- frontend/react/src/ui/panels/awacsmenu.tsx | 2 +- .../react/src/ui/panels/components/menu.tsx | 94 +++++++++++-------- .../react/src/ui/panels/controlspanel.tsx | 7 +- .../react/src/ui/panels/coordinatespanel.tsx | 56 +++++++---- frontend/react/src/ui/panels/drawingmenu.tsx | 1 - frontend/react/src/ui/panels/header.tsx | 5 +- frontend/react/src/ui/panels/jtacmenu.tsx | 2 +- frontend/react/src/ui/panels/minimappanel.tsx | 94 +++++++++++-------- .../src/ui/panels/radiossummarypanel.tsx | 6 +- frontend/react/src/ui/panels/spawnmenu.tsx | 1 - .../react/src/ui/panels/unitcontrolmenu.tsx | 1 - frontend/react/src/ui/ui.tsx | 4 +- 17 files changed, 211 insertions(+), 116 deletions(-) diff --git a/frontend/react/src/map/boxselect.ts b/frontend/react/src/map/boxselect.ts index edfa9635..f4829029 100644 --- a/frontend/react/src/map/boxselect.ts +++ b/frontend/react/src/map/boxselect.ts @@ -17,10 +17,12 @@ export var BoxSelect = Handler.extend({ addHooks: function () { DomEvent.on(this._container, "mousedown", this._onMouseDown, this); + DomEvent.on(this._container, "touchstart", this._onMouseDown, this); }, removeHooks: function () { DomEvent.off(this._container, "mousedown", this._onMouseDown, this); + DomEvent.off(this._container, "touchend", this._onMouseDown, this); }, moved: function () { @@ -37,7 +39,7 @@ export var BoxSelect = Handler.extend({ }, _onMouseDown: function (e: any) { - if (this._map.getSelectionEnabled() && e.button == 0) { + if (this._map.getSelectionEnabled() && (e.button == 0 || e.type === "touchstart")) { if (this._moved) this._finish(); DomUtil.disableImageDrag(); @@ -64,7 +66,7 @@ export var BoxSelect = Handler.extend({ }, _onMouseUp: function (e: any) { - if (e.button !== 0) return; + if (e.button !== 0 && e.type !== "touchend") return; window.setTimeout(Util.bind(this._finish, this), 0); if (!this._moved) return; var bounds = new LatLngBounds(this._map.containerPointToLatLng(this._startPoint), this._map.containerPointToLatLng(this._point)); diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 4ec202ca..28c0f4e5 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -209,7 +209,9 @@ export class Map extends L.Map { this.on("selectionend", (e: any) => this.#onSelectionEnd(e)); this.on("mouseup", (e: any) => this.#onMouseUp(e)); + this.on("touchend", (e: any) => this.#onMouseUp(e)); this.on("mousedown", (e: any) => this.#onMouseDown(e)); + this.on("touchstart", (e: any) => this.#onMouseDown(e)); this.on("dblclick", (e: any) => this.#onDoubleClick(e)); this.on("click", (e: any) => e.originalEvent.preventDefault()); this.on("contextmenu", (e: any) => e.originalEvent.preventDefault()); @@ -810,6 +812,10 @@ export class Map extends L.Map { setSelectionEnabled(selectionEnabled: boolean) { this.#selectionEnabled = selectionEnabled; + + if (selectionEnabled) this.dragging.disable(); + else this.dragging.enable(); + SelectionEnabledChangedEvent.dispatch(selectionEnabled); } @@ -963,6 +969,9 @@ export class Map extends L.Map { #onSelectionEnd(e: any) { getApp().getUnitsManager().selectFromBounds(e.selectionBounds); + // Autodisable the selection mode if touchscreen + if ("ontouchstart" in window) this.setSelectionEnabled(false); + /* Delay the event so that any other event in the queue still sees the map in selection mode */ window.setTimeout(() => { this.#isSelecting = false; @@ -997,7 +1006,7 @@ export class Map extends L.Map { } #onMouseDown(e: any) { - if (e.originalEvent.button === 1) { + if (e.originalEvent?.button === 1) { this.dragging.disable(); } // Disable dragging when right clicking diff --git a/frontend/react/src/ui/components/ollocation.tsx b/frontend/react/src/ui/components/ollocation.tsx index 82ffb6e2..34e142f5 100644 --- a/frontend/react/src/ui/components/ollocation.tsx +++ b/frontend/react/src/ui/components/ollocation.tsx @@ -12,7 +12,14 @@ export function OlLocation(props: { location: LatLng; className?: string; refere ${props.className ?? ""} my-auto cursor-pointer bg-olympus-400 p-2 text-white `} - onClick={props.onClick ? props.onClick : () => setReferenceSystem("LatLngDec")} + onClick={ + props.onClick + ? props.onClick + : (ev) => { + setReferenceSystem("LatLngDec"); + ev.stopPropagation(); + } + } > setReferenceSystem("LatLngDMS")} + onClick={ + props.onClick + ? props.onClick + : (ev) => { + setReferenceSystem("LatLngDMS"); + ev.stopPropagation(); + } + } >
setReferenceSystem("MGRS")} + onClick={ + props.onClick + ? props.onClick + : (ev) => { + setReferenceSystem("MGRS"); + ev.stopPropagation(); + } + } >
{ + setIsTouchscreen("ontouchstart" in window); + }, []); + var contentRef = useRef(null); function setPosition(content: HTMLDivElement, button: HTMLButtonElement) { @@ -106,7 +112,7 @@ export function OlTooltip(props: { }); return ( - props.content !== "" && ( + props.content !== "" && !isTouchscreen && (
void; childre } return ( - +
void; children? }, []); return ( - +
void; - canBeHidden?: boolean; onBack?: () => void; showBackButton?: boolean; children?: JSX.Element | JSX.Element[]; - wiki?: () => (JSX.Element | JSX.Element[]); + wiki?: () => JSX.Element | JSX.Element[]; }) { const [hide, setHide] = useState(true); const [wiki, setWiki] = useState(false); if (!props.open && hide) setHide(false); + useEffect(() => { + if (window.innerWidth > 640) setHide(false); + }, [props.open]); + return (
+ {props.open && ( +
+
setHide(!hide)} + > + +
+
+ )}
+ setHide(true)} + icon={faEyeSlash} + className={` + flex cursor-pointer items-center justify-center rounded-md p-2 + text-lg + dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white + hover:bg-gray-200 + `} + />
-
-
- {props.wiki ? props.wiki() :
Work in progress
} -
-
{props.children}
+
+
+ {props.wiki ? props.wiki() :
Work in progress
} +
+
+ {props.children} +
- {props.canBeHidden == true && ( -
setHide(!hide)} - > - {hide ? ( - - ) : ( - - )} -
- )}
); } diff --git a/frontend/react/src/ui/panels/controlspanel.tsx b/frontend/react/src/ui/panels/controlspanel.tsx index 411bd77a..8dca56e9 100644 --- a/frontend/react/src/ui/panels/controlspanel.tsx +++ b/frontend/react/src/ui/panels/controlspanel.tsx @@ -5,6 +5,7 @@ import { DrawSubState, MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusS import { AppStateChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events"; import { ContextActionSet } from "../../unit/contextactionset"; import { MapToolBar } from "./maptoolbar"; +import { CoordinatesPanel } from "./coordinatespanel"; export function ControlsPanel(props: {}) { const [controls, setControls] = useState( @@ -200,12 +201,12 @@ export function ControlsPanel(props: {}) { className={` absolute right-[0px] top-16 ${mapOptions.showMinimap ? `bottom-[233px]` : `bottom-[65px]`} - pointer-events-none flex w-[310px] flex-col items-center justify-between + pointer-events-none flex w-[288px] flex-col items-center justify-between gap-1 p-3 text-sm `} > - {controls?.map((control) => { + {/*controls?.map((control) => { return (
); - })} + })*/}
); } diff --git a/frontend/react/src/ui/panels/coordinatespanel.tsx b/frontend/react/src/ui/panels/coordinatespanel.tsx index b33a728c..c1fe6dee 100644 --- a/frontend/react/src/ui/panels/coordinatespanel.tsx +++ b/frontend/react/src/ui/panels/coordinatespanel.tsx @@ -12,6 +12,7 @@ export function CoordinatesPanel(props: {}) { const [elevation, setElevation] = useState(0); const [bullseyes, setBullseyes] = useState(null as null | { [name: string]: Bullseye }); const [selectedUnits, setSelectedUnits] = useState([] as Unit[]); + const [open, setOpen] = useState(true); useEffect(() => { MouseMovedEvent.on((latlng, elevation) => { @@ -27,18 +28,30 @@ export function CoordinatesPanel(props: {}) { return (
setOpen(!open)} > - {bullseyes && ( -
+
+ {open ? ( + + ) : ( + + )} +
+ {open && bullseyes && ( +
{bullseyes[2] && ( @@ -84,18 +97,27 @@ export function CoordinatesPanel(props: {}) {
)} -
+
- - - -
{mToFt(elevation).toFixed()}ft
+ + {open && ( +
+ + + +
{mToFt(elevation).toFixed()}ft
+
+ )}
); } diff --git a/frontend/react/src/ui/panels/drawingmenu.tsx b/frontend/react/src/ui/panels/drawingmenu.tsx index d3ea79ba..56e1737a 100644 --- a/frontend/react/src/ui/panels/drawingmenu.tsx +++ b/frontend/react/src/ui/panels/drawingmenu.tsx @@ -147,7 +147,6 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { open={props.open} title="Draw" onClose={props.onClose} - canBeHidden={true} showBackButton={appSubState !== DrawSubState.NO_SUBSTATE} onBack={() => { getApp().getCoalitionAreasManager().setSelectedArea(null); diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 8429e62b..e18b53aa 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -102,7 +102,8 @@ export function Header() { return (
@@ -147,6 +148,7 @@ export function Header() { {IP}
+
{savingSessionData ? (
@@ -162,6 +164,7 @@ export function Header() {
)} +
{commandModeOptions.commandMode === BLUE_COMMANDER && ( diff --git a/frontend/react/src/ui/panels/jtacmenu.tsx b/frontend/react/src/ui/panels/jtacmenu.tsx index 3990f6c9..795980d6 100644 --- a/frontend/react/src/ui/panels/jtacmenu.tsx +++ b/frontend/react/src/ui/panels/jtacmenu.tsx @@ -60,7 +60,7 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?: let targetPosition = (targetUnit ? targetUnit.getPosition() : targetLocation) ?? new LatLng(0, 0); return ( - +
- {!serverStatus.connected ? ( -
-
- Server disconnected -
- ) : serverStatus.paused ? ( -
-
- Server paused -
- ) : ( - <> -
- FPS: - - {serverStatus.frameRate} - + + +
{ + getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap); + }}> + {!serverStatus.connected ? ( +
+
+ Server disconnected
-
- Load: - - {serverStatus.load} - + ) : serverStatus.paused ? ( +
+
+ Server paused
-
setShowMissionTime(!showMissionTime)}> - {showMissionTime ? "MT" : "ET"}: {timeString} -
-
- - )} - {mapOptions.showMinimap ? ( - getApp().getMap().setOption("showMinimap", false)} /> - ) : ( - getApp().getMap().setOption("showMinimap", true)} /> - )} + ) : ( + <> +
+ FPS: + + {serverStatus.frameRate} + +
+
+ Load: + + {serverStatus.load} + +
+
{ + setShowMissionTime(!showMissionTime); + ev.stopPropagation(); + }} + > + {showMissionTime ? "MT" : "ET"}: {timeString} +
+ + )} + {mapOptions.showMinimap ? ( + + ) : ( + + )} +
); } diff --git a/frontend/react/src/ui/panels/radiossummarypanel.tsx b/frontend/react/src/ui/panels/radiossummarypanel.tsx index e4324d97..77fa7ae1 100644 --- a/frontend/react/src/ui/panels/radiossummarypanel.tsx +++ b/frontend/react/src/ui/panels/radiossummarypanel.tsx @@ -19,12 +19,10 @@ export function RadiosSummaryPanel(props: {}) { {audioSinks.length > 0 && (
-
+
{audioSinks.filter((audioSinks) => audioSinks instanceof RadioSink).length > 0 && audioSinks diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index dc0c7001..3437c8f5 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -113,7 +113,6 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? {...props} title="Spawn menu" showBackButton={blueprint !== null || effect !== null} - canBeHidden={true} onBack={() => { getApp().setState(OlympusState.SPAWN); setBlueprint(null); diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index b9297688..6e662acb 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -295,7 +295,6 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { open={props.open} title={selectedUnits.length > 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`} onClose={props.onClose} - canBeHidden={true} wiki={() => { return (
- - + + From 4350cd93e52fdff376cc0c3a14343d2ceb213d5e Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Wed, 19 Mar 2025 12:59:00 +0100 Subject: [PATCH 24/33] fix: Minor package updates Possible breaking change for Vite, to be monitored --- frontend/react/package.json | 14 ++++++-------- frontend/server/package.json | 14 +++++++------- manager/package.json | 22 +++++++++++----------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/frontend/react/package.json b/frontend/react/package.json index dc94f284..1c961ff5 100644 --- a/frontend/react/package.json +++ b/frontend/react/package.json @@ -8,13 +8,6 @@ "build-release": "vite build", "preview": "vite preview" }, - "dependencies": { - "axios": "^1.8.1", - "chart.js": "^4.4.7", - "react-chartjs-2": "^5.3.0", - "react-circular-progressbar": "^2.1.0", - "react-clock": "^5.1.0" - }, "devDependencies": { "@eslint/js": "^9.6.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", @@ -35,7 +28,9 @@ "@typescript-eslint/parser": "^7.14.1", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.19", + "axios": "^1.8.1", "buffer": "^6.0.3", + "chart.js": "^4.4.7", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -54,6 +49,9 @@ "postcss": "^8.4.38", "prettier": "^3.3.2", "react": "^18.2.0", + "react-chartjs-2": "^5.3.0", + "react-circular-progressbar": "^2.1.0", + "react-clock": "^5.1.0", "react-dom": "^18.2.0", "react-icons": "^5.0.1", "react-leaflet": "^4.2.1", @@ -61,7 +59,7 @@ "turf": "^3.0.14", "typescript-eslint": "^7.14.1", "usng": "^0.3.0", - "vite": "^5.2.0", + "vite": "^6.2.2", "vite-plugin-externals": "^0.6.2", "vite-plugin-file": "^1.0.5", "web-audio-peak-meter": "^3.1.0" diff --git a/frontend/server/package.json b/frontend/server/package.json index 0c1daaf5..232d7d57 100644 --- a/frontend/server/package.json +++ b/frontend/server/package.json @@ -11,15 +11,15 @@ }, "private": true, "dependencies": { - "@google-cloud/speech": "^6.7.0", - "@google-cloud/text-to-speech": "^5.6.0", + "@google-cloud/speech": "^6.7.1", + "@google-cloud/text-to-speech": "^5.8.1", "appjs": "^0.0.20", "appjs-win32": "^0.0.19", "body-parser": "^1.20.3", "cors": "^2.8.5", "debug": "~4.4.0", "ejs": "^3.1.10", - "electron": "^33.2.1", + "electron": "^33.4.5", "express": "^4.21.2", "express-basic-auth": "^1.2.1", "http-proxy-middleware": "^3.0.3", @@ -31,14 +31,14 @@ "sha256": "^0.2.0", "srtm-elevation": "^2.1.2", "tcp-ping-port": "^1.0.2", - "uuid": "^11.0.3", - "whatwg-url": "^14.1.0", - "ws": "^8.18.0", + "uuid": "^11.1.0", + "whatwg-url": "^14.2.0", + "ws": "^8.18.1", "yargs": "^17.7.2" }, "devDependencies": { "ts-node": "^10.9.2", - "typescript": "^5.7.2" + "typescript": "^5.8.2" }, "overrides": { "node-fetch": "^2.7.0" diff --git a/manager/package.json b/manager/package.json index fda57f6e..9c768c7c 100644 --- a/manager/package.json +++ b/manager/package.json @@ -1,6 +1,6 @@ { "name": "dcsolympus_manager", - "version": "1.0.0", + "version": "{{OLYMPUS_VERSION_NUMBER}}", "description": "", "main": "main.js", "scripts": { @@ -11,20 +11,20 @@ "license": "ISC", "dependencies": { "@electron/remote": "^2.1.2", - "adm-zip": "^0.5.10", - "create-desktop-shortcuts": "^1.10.1", + "adm-zip": "^0.5.16", + "create-desktop-shortcuts": "^1.11.0", "dir-compare": "^4.2.0", - "ejs": "^3.1.9", - "electron": "^28.0.0", - "find-process": "^1.4.7", - "follow-redirects": "^1.15.4", - "octokit": "^3.1.2", - "portfinder": "^1.0.32", - "regedit": "^5.1.2", + "ejs": "^3.1.10", + "electron": "^28.3.3", + "find-process": "^1.4.10", + "follow-redirects": "^1.15.9", + "octokit": "^3.2.1", + "portfinder": "^1.0.35", + "regedit": "^5.1.3", "sha256": "^0.2.0", "win-version-info": "^6.0.1" }, "devDependencies": { - "nodemon": "^3.0.2" + "nodemon": "^3.1.9" } } From 48d64078d8c0fc05363ffbb5f8bb5e91463f65d4 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Wed, 19 Mar 2025 16:23:27 +0100 Subject: [PATCH 25/33] fix: fixed wiki mode for small screens --- .../react/src/ui/modals/trainingmodal.tsx | 26 +++++++++---------- .../react/src/ui/panels/components/menu.tsx | 19 +++++++++----- .../react/src/ui/panels/unitcontrolmenu.tsx | 1 + 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/frontend/react/src/ui/modals/trainingmodal.tsx b/frontend/react/src/ui/modals/trainingmodal.tsx index ef55de5f..2cfe79c1 100644 --- a/frontend/react/src/ui/modals/trainingmodal.tsx +++ b/frontend/react/src/ui/modals/trainingmodal.tsx @@ -19,7 +19,7 @@ export function TrainingModal(props: { open: boolean }) { <> {step === 0 && ( -
+
{step === 1 && ( -
+
{step === 2 && ( -
+
{step === 3 && ( -
+
{step === 4 && ( -
+
{step === 5 && ( -
+
{step === 6 && ( -
+
{step === 7 && ( -
+
{step === 8 && ( -
+
{step === 9 && ( -
+
{step === 10 && ( -
+

The unit marker (2 of 2)

The unit marker has a symbol showing the unit state, i.e. what instruction it is performing. These are all the possible values:

-
+

@@ -475,7 +475,7 @@ export function TrainingModal(props: { open: boolean }) { )} {step > 0 && ( -

+
{[...Array(MAX_STEPS).keys()].map((i) => (
void; showBackButton?: boolean; children?: JSX.Element | JSX.Element[]; + autohide?: boolean; wiki?: () => JSX.Element | JSX.Element[]; }) { const [hide, setHide] = useState(true); const [wiki, setWiki] = useState(false); - if (!props.open && hide) setHide(false); - useEffect(() => { - if (window.innerWidth > 640) setHide(false); + if (props.autohide) { + if (window.innerWidth > 640) setHide(false); + if (!props.open) setHide(true); + } else { + setHide(false); + } }, [props.open]); return ( @@ -108,12 +112,16 @@ export function Menu(props: { `} /> -
+
{props.wiki ? props.wiki() :
Work in progress
} @@ -121,7 +129,6 @@ export function Menu(props: {
diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 6e662acb..0ee452ea 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -295,6 +295,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { open={props.open} title={selectedUnits.length > 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`} onClose={props.onClose} + autohide={true} wiki={() => { return (
Date: Wed, 19 Mar 2025 16:33:10 +0100 Subject: [PATCH 26/33] fix: Coalition of unit bullseye info toggle now working --- frontend/react/src/ui/panels/optionsmenu.tsx | 30 ++++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/frontend/react/src/ui/panels/optionsmenu.tsx b/frontend/react/src/ui/panels/optionsmenu.tsx index 53946a1c..f33f11ab 100644 --- a/frontend/react/src/ui/panels/optionsmenu.tsx +++ b/frontend/react/src/ui/panels/optionsmenu.tsx @@ -41,17 +41,16 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre }, // Specify the content type }; - fetch(`./admin/config`, requestOptions) - .then((response) => { - if (response.status === 200) { - console.log(`Admin password correct`); - getApp().setAdminPassword(password); - getApp().setState(OlympusState.ADMIN) - return response.json(); - } else { - throw new Error("Admin password incorrect"); - } - }) + fetch(`./admin/config`, requestOptions).then((response) => { + if (response.status === 200) { + console.log(`Admin password correct`); + getApp().setAdminPassword(password); + getApp().setState(OlympusState.ADMIN); + return response.json(); + } else { + throw new Error("Admin password incorrect"); + } + }); }; useEffect(() => { @@ -215,7 +214,14 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre >
- {}} coalition={mapOptions.AWACSCoalition} /> + { + mapOptions.AWACSCoalition === "blue" && getApp().getMap().setOption("AWACSCoalition", "neutral"); + mapOptions.AWACSCoalition === "neutral" && getApp().getMap().setOption("AWACSCoalition", "red"); + mapOptions.AWACSCoalition === "red" && getApp().getMap().setOption("AWACSCoalition", "blue"); + }} + coalition={mapOptions.AWACSCoalition} + /> Coalition of unit bullseye info
From 2d26862b6c15a8b314b39de45923fa25bf3bdbe2 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Wed, 19 Mar 2025 17:23:17 +0100 Subject: [PATCH 27/33] fix: Error in admin panel inputs fixed, plus improvements to modal panels --- frontend/react/src/ui/modals/adminmodal.tsx | 14 ++++---- .../react/src/ui/modals/components/modal.tsx | 23 +++++++------ .../react/src/ui/modals/importexportmodal.tsx | 2 ++ frontend/react/src/ui/modals/keybindmodal.tsx | 6 ++-- .../src/ui/modals/protectionpromptmodal.tsx | 1 + frontend/react/src/ui/modals/warningmodal.tsx | 30 +++++++---------- frontend/react/src/ui/panels/header.tsx | 32 +++++++++++-------- 7 files changed, 55 insertions(+), 53 deletions(-) diff --git a/frontend/react/src/ui/modals/adminmodal.tsx b/frontend/react/src/ui/modals/adminmodal.tsx index 1f6bf47c..969b4e66 100644 --- a/frontend/react/src/ui/modals/adminmodal.tsx +++ b/frontend/react/src/ui/modals/adminmodal.tsx @@ -76,11 +76,11 @@ export function AdminModal(props: { open: boolean }) { return ( - -
-
+ +
+
Groups:
-
+
{configs.groups && Object.keys(configs.groups).map((group: any) => { return ( @@ -142,7 +142,7 @@ export function AdminModal(props: { open: boolean }) { type="text" autoComplete="new-password" onChange={(ev) => { - setNewUserName(ev.currentTarget.value); + setNewGroupName(ev.currentTarget.value); }} className={` rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm @@ -173,9 +173,9 @@ export function AdminModal(props: { open: boolean }) {
-
+
Users:
-
+
{configs.users && Object.keys(configs.users).map((user: any) => { return ( diff --git a/frontend/react/src/ui/modals/components/modal.tsx b/frontend/react/src/ui/modals/components/modal.tsx index b8734b8f..2e5eb931 100644 --- a/frontend/react/src/ui/modals/components/modal.tsx +++ b/frontend/react/src/ui/modals/components/modal.tsx @@ -4,7 +4,7 @@ import { FaXmark } from "react-icons/fa6"; import { getApp, OlympusApp } from "../../../olympusapp"; import { OlympusState } from "../../../constants/constants"; -export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Element[]; className?: string }) { +export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Element[]; className?: string; size?: "sm" | "md" | "lg" | "full" }) { const [splash, setSplash] = useState(Math.ceil(Math.random() * 7)); useEffect(() => { @@ -18,19 +18,18 @@ export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Eleme
- +
+
+
); } diff --git a/frontend/react/src/ui/modals/keybindmodal.tsx b/frontend/react/src/ui/modals/keybindmodal.tsx index f39da88b..fd98864d 100644 --- a/frontend/react/src/ui/modals/keybindmodal.tsx +++ b/frontend/react/src/ui/modals/keybindmodal.tsx @@ -61,7 +61,8 @@ export function KeybindModal(props: { open: boolean }) { } return ( - + +
)}
-
+
{shortcut && (
+
); } diff --git a/frontend/react/src/ui/modals/protectionpromptmodal.tsx b/frontend/react/src/ui/modals/protectionpromptmodal.tsx index 6017c8e8..a0a680f5 100644 --- a/frontend/react/src/ui/modals/protectionpromptmodal.tsx +++ b/frontend/react/src/ui/modals/protectionpromptmodal.tsx @@ -10,6 +10,7 @@ export function ProtectionPromptModal(props: { open: boolean }) { return (
diff --git a/frontend/react/src/ui/modals/warningmodal.tsx b/frontend/react/src/ui/modals/warningmodal.tsx index 2e3c349c..fee3b9d8 100644 --- a/frontend/react/src/ui/modals/warningmodal.tsx +++ b/frontend/react/src/ui/modals/warningmodal.tsx @@ -46,19 +46,12 @@ export function WarningModal(props: { open: boolean }) { ); break; case WarningSubstate.NOT_SECURE: - case WarningSubstate.NOT_CHROME: warningText = (
Your connection to DCS Olympus is not secure. To protect your personal data some advanced DCS Olympus features like the camera plugin or the audio backend have been disabled. - To solve this issue, DCS Olympus should be served using the{" "} - - https - {" "} - protocol. + To solve this issue, DCS Olympus should be served using the https protocol. To do so, we suggest using a dedicated server and a reverse proxy with SSL enabled.
@@ -73,27 +66,26 @@ export function WarningModal(props: { open: boolean }) {
); break; - case WarningSubstate.ERROR_UPLOADING_CONFIG: - warningText = ( -
- An error has occurred uploading the admin configuration. - - -
- ); - break; + case WarningSubstate.ERROR_UPLOADING_CONFIG: + warningText = ( +
+ An error has occurred uploading the admin configuration. + +
+ ); + break; default: break; } } return ( - +
Warning
-
{warningText}
+
{warningText}
- {savingSessionData ? ( -
- -
- ) : ( -
- - + +
+ ) : ( +
+ + - -
- )} + /> + +
+ )}
From 0ef5de51c4a13504cae0400b1d9de0e01f220c8e Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Wed, 19 Mar 2025 17:39:59 +0100 Subject: [PATCH 28/33] fix: Drawings visibility not aligned with map option --- frontend/react/src/map/drawings/drawingsmanager.ts | 2 ++ frontend/react/src/ui/panels/drawingmenu.tsx | 7 ++++++- frontend/react/src/ui/panels/header.tsx | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/react/src/map/drawings/drawingsmanager.ts b/frontend/react/src/map/drawings/drawingsmanager.ts index af74ffce..7c823bfa 100644 --- a/frontend/react/src/map/drawings/drawingsmanager.ts +++ b/frontend/react/src/map/drawings/drawingsmanager.ts @@ -719,6 +719,7 @@ export class DrawingsManager { SessionDataLoadedEvent.on((sessionData) => { this.#sessionDataDrawings = sessionData.drawings ?? {}; if (this.#initialized) if (this.#sessionDataDrawings["Mission drawings"]) this.#drawingsContainer.fromJSON(this.#sessionDataDrawings["Mission drawings"]); + this.#drawingsContainer.setVisibility(getApp().getMap().getOptions().showMissionDrawings); }); } @@ -727,6 +728,7 @@ export class DrawingsManager { this.#drawingsContainer.initFromData(data.drawings); if (data.drawings.navpoints) this.#drawingsContainer.initNavpoints(data.drawings.navpoints); if (this.#sessionDataDrawings["Mission drawings"]) this.#drawingsContainer.fromJSON(this.#sessionDataDrawings["Mission drawings"]); + this.#drawingsContainer.setVisibility(getApp().getMap().getOptions().showMissionDrawings); DrawingsInitEvent.dispatch(this.#drawingsContainer); this.#initialized = true; return true; diff --git a/frontend/react/src/ui/panels/drawingmenu.tsx b/frontend/react/src/ui/panels/drawingmenu.tsx index 56e1737a..a410cded 100644 --- a/frontend/react/src/ui/panels/drawingmenu.tsx +++ b/frontend/react/src/ui/panels/drawingmenu.tsx @@ -92,7 +92,12 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { hover:scale-125 hover:text-gray-200 `} onClick={() => { - container.setVisibility(!container.getVisibility(), true); + + if (container === mainDrawingsContainer.container) { + getApp().getMap().setOption("showMissionDrawings", !getApp().getMap().getOptions().showMissionDrawings); + } else { + container.setVisibility(!container.getVisibility(), true); + } }} />
{ HiddenTypesChangedEvent.on((hiddenTypes) => setMapHiddenTypes({ ...hiddenTypes })); - MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); + MapOptionsChangedEvent.on((mapOptions) => { + setMapOptions({ ...mapOptions }) + }); MapSourceChangedEvent.on((source) => setMapSource(source)); ConfigLoadedEvent.on((config: OlympusConfig) => { // Timeout needed to make sure the map configuration has updated From 791b1fc4abc3ad7aa33330e230b12638b15b1792 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 20 Mar 2025 10:49:07 +0100 Subject: [PATCH 29/33] fix: Login modal rendered twice Also added ability to login by username or by role --- .../react/src/ui/modals/components/modal.tsx | 80 +++++-- frontend/react/src/ui/modals/loginmodal.tsx | 204 ++++++++++++------ frontend/react/src/ui/ui.tsx | 1 - 3 files changed, 195 insertions(+), 90 deletions(-) diff --git a/frontend/react/src/ui/modals/components/modal.tsx b/frontend/react/src/ui/modals/components/modal.tsx index 2e5eb931..2ad7fb53 100644 --- a/frontend/react/src/ui/modals/components/modal.tsx +++ b/frontend/react/src/ui/modals/components/modal.tsx @@ -1,10 +1,16 @@ import React, { useEffect, useState } from "react"; import { ModalEvent } from "../../../events"; import { FaXmark } from "react-icons/fa6"; -import { getApp, OlympusApp } from "../../../olympusapp"; +import { getApp } from "../../../olympusapp"; import { OlympusState } from "../../../constants/constants"; -export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Element[]; className?: string; size?: "sm" | "md" | "lg" | "full" }) { +export function Modal(props: { + open: boolean; + children?: JSX.Element | JSX.Element[]; + className?: string; + size?: "sm" | "md" | "lg" | "full"; + disableClose?: boolean; +}) { const [splash, setSplash] = useState(Math.ceil(Math.random() * 7)); useEffect(() => { @@ -15,21 +21,47 @@ export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Eleme <> {props.open && ( <> -
+
- +
{props.children} -
- { - getApp().setState(OlympusState.IDLE); - }} - />{" "} -
+ {!props.disableClose && ( +
+ { + getApp().setState(OlympusState.IDLE); + }} + />{" "} +
+ )}
diff --git a/frontend/react/src/ui/modals/loginmodal.tsx b/frontend/react/src/ui/modals/loginmodal.tsx index df5afb0f..402d8607 100644 --- a/frontend/react/src/ui/modals/loginmodal.tsx +++ b/frontend/react/src/ui/modals/loginmodal.tsx @@ -3,12 +3,12 @@ import { Modal } from "./components/modal"; import { Card } from "./components/card"; import { ErrorCallout } from "../components/olcallout"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons"; +import { faArrowLeft, faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons"; import { getApp, VERSION } from "../../olympusapp"; import { sha256 } from "js-sha256"; import { LoginSubState, NO_SUBSTATE, OlympusState } from "../../constants/constants"; import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; -import { AppStateChangedEvent, EnabledCommandModesChangedEvent, MissionDataChangedEvent, WrongCredentialsEvent } from "../../events"; +import { AppStateChangedEvent, EnabledCommandModesChangedEvent, WrongCredentialsEvent } from "../../events"; export function LoginModal(props: { open: boolean }) { const [subState, setSubState] = useState(NO_SUBSTATE); @@ -18,6 +18,7 @@ export function LoginModal(props: { open: boolean }) { const [loginError, setLoginError] = useState(false); const [commandModes, setCommandModes] = useState(null as null | string[]); const [activeCommandMode, setActiveCommandMode] = useState(null as null | string); + const [loginByRole, setLoginByRole] = useState(true); useEffect(() => { AppStateChangedEvent.on((state, subState) => { @@ -29,6 +30,11 @@ export function LoginModal(props: { open: boolean }) { }); }, []); + const updateUsername = useCallback(() => { + loginByRole ? setUsername("Game master") : setUsername(""); + }, [loginByRole]); + useEffect(updateUsername, [loginByRole]); + const usernameCallback = useCallback(() => getApp()?.getServerManager().setUsername(username), [username]); useEffect(usernameCallback, [username]); @@ -80,7 +86,7 @@ export function LoginModal(props: { open: boolean }) { useEffect(subStateCallback, [subState]); return ( - +
{!checkingPassword ? ( <> -
-
- Connect to -
-
- {window.location.toString()} -
-
{subState === LoginSubState.CREDENTIALS && ( <> -
-
+
+ {loginByRole ? ( + <> + + + {setUsername("Game master")}}>Game master + {setUsername("Blue commander")}}>Blue commander + {setUsername("Red commander")}}>Red commander + + + + ) : ( + <> + + setUsername(ev.currentTarget.value)} + className={` + block w-full rounded-lg border border-gray-300 + bg-gray-50 p-2.5 text-sm text-gray-900 + dark:border-gray-600 dark:bg-gray-700 + dark:text-white dark:placeholder-gray-400 + dark:focus:border-blue-500 + dark:focus:ring-blue-500 + focus:border-blue-500 focus:ring-blue-500 + `} + placeholder="Enter username" + value={username} + required + /> + + )}
+ )} @@ -309,6 +380,7 @@ export function LoginModal(props: { open: boolean }) {
)}
+
-
+
YouTube Video Guide
@@ -348,9 +420,9 @@ export function LoginModal(props: { open: boolean }) { object-cover `} > -
+
Wiki Guide
diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index 8a3cecf5..ff53ff02 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -73,7 +73,6 @@ export function UI() { - From 0765459cfd152e93678499b038d67ab5f827ef2e Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 20 Mar 2025 16:51:58 +0100 Subject: [PATCH 30/33] feat: Added wiki modes to audio, drawing and gamemaster menus. Feat: improved looks of gamemaster menu --- frontend/react/src/other/utils.ts | 8 + frontend/react/src/ui/panels/audiomenu.tsx | 77 ++++++- .../react/src/ui/panels/components/menu.tsx | 45 ++-- frontend/react/src/ui/panels/drawingmenu.tsx | 58 ++++- .../react/src/ui/panels/gamemastermenu.tsx | 207 ++++++++++-------- 5 files changed, 280 insertions(+), 115 deletions(-) diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index b4506c10..df0b6577 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -754,4 +754,12 @@ export async function getWikipediaSummary(unitName: string): Promise void; children? const lineDistance = (paddingRight - 40) / lineCounters[lineCounters.length - 1]; return ( - + { + return ( +
+

Audio menu

+
+ 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. +
+
+ 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. +
+
+ For security reasons, the audio backend will only work if the page is served over HTTPS. +
+

Managing the audio backend

+
+ 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 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. +
+

Creating audio sources

+
+ 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. +
+
+ 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 */} +
+
+ 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. +
+

Creating radios and loudspeakers

+
+ 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. +
+
+ 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. +
+
+ You have three options to transmit on the radio: +
+
  • By clicking on the "Talk on frequency" button on the radio panel. This will enable continuous transmission and will remain "on" until clicked again.
  • +
  • By clicking on the "Push to talk" button located over the mouse coordinates panel, on the bottom right corner of the map.
  • +
  • By using the "Push to talk" keyboard shortcuts, which can be edited in the options menu.
  • +
    +
    +
    + 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. +
    +
    + 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). +
    +

    Connecting sources and radios/loudspeakers

    +
    + 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. +
    +
    + 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. +
    +
    + By connecting multiple sources to the same radio/loudspeaker, you can create complex audio setups, like playing background music while transmitting on the radio. +
    +
    + ); + }} + >
    diff --git a/frontend/react/src/ui/panels/components/menu.tsx b/frontend/react/src/ui/panels/components/menu.tsx index 7da2ed98..f7f0bbbf 100644 --- a/frontend/react/src/ui/panels/components/menu.tsx +++ b/frontend/react/src/ui/panels/components/menu.tsx @@ -12,6 +12,7 @@ export function Menu(props: { children?: JSX.Element | JSX.Element[]; autohide?: boolean; wiki?: () => JSX.Element | JSX.Element[]; + wikiDisabled?: boolean; }) { const [hide, setHide] = useState(true); const [wiki, setWiki] = useState(false); @@ -56,9 +57,8 @@ export function Menu(props: {
    )} {props.title} - setWiki(!wiki)} - icon={faCircleQuestion} - className={` - ml-auto flex cursor-pointer items-center justify-center rounded-md - p-2 text-lg - dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white - hover:bg-gray-200 - `} - /> + {!(props.wikiDisabled === true) && ( + setWiki(!wiki)} + icon={faCircleQuestion} + className={` + ml-auto flex cursor-pointer items-center justify-center + rounded-md p-2 text-lg + dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white + hover:bg-gray-200 + `} + /> + )} setHide(true)} icon={faEyeSlash} @@ -112,16 +114,18 @@ export function Menu(props: { `} /> -
    +
    {props.wiki ? props.wiki() :
    Work in progress
    } @@ -129,6 +133,7 @@ export function Menu(props: {
    diff --git a/frontend/react/src/ui/panels/drawingmenu.tsx b/frontend/react/src/ui/panels/drawingmenu.tsx index a410cded..a873d37a 100644 --- a/frontend/react/src/ui/panels/drawingmenu.tsx +++ b/frontend/react/src/ui/panels/drawingmenu.tsx @@ -92,7 +92,6 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { hover:scale-125 hover:text-gray-200 `} onClick={() => { - if (container === mainDrawingsContainer.container) { getApp().getMap().setOption("showMissionDrawings", !getApp().getMap().getOptions().showMissionDrawings); } else { @@ -157,6 +156,55 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { getApp().getCoalitionAreasManager().setSelectedArea(null); getApp().setState(OlympusState.DRAW, DrawSubState.NO_SUBSTATE); }} + wiki={() => { + return ( +
    +

    Drawing menu

    +
    + The drawing menu allows you to create and manage custom drawings, such as polygons and circles, and to generate IADS (Integrated Air Defense + System) areas. Moreover, you can manage the visibility and opacity of mission drawings, i.e. drawings from the Mission Editor. +
    +

    Custom drawings and IADS

    +
    + To create a custom drawing, click on the 'Add polygon' or 'Add circle' buttons, then click on the map to add polygons or to move the drawing. + Double-click on the map to finish your creation. You can then edit the drawing by clicking on it. You can also move it up or down in the list, or + delete it. +
    +
    + You can change the name and the coalition of the area. You can also generate an IADS area by selecting the types, eras, and ranges of units you + want to include in the area. You can also set the density and distribution of the IADS. If you check the 'Force coalition appropriate units' box, + the IADS will only include units that are appropriate for the coalition of the area (e.g. Hawk SAMs for {""} + blue and SA-6 SAMs for{" "} + + red + + ). +
    +
    + The IADS generator will create a random distribution of units in the area, based on the density and distribution you set. Units will be + concentrated around cities, and airbases that belong to the selected coalition. +
    +

    Mission drawings

    +
    + You can manage the visibility and opacity of mission drawings by clicking on the eye icon. Moreover, you can change the opacity of the drawing by + using the slider. You can also hide or show all the drawings in a container. +
    +
    + You can search for a specific drawing by typing in the search bar. The search is case-insensitive and will match any part of the drawing name. +
    +
    + Any change you make is persistent and will be saved for the next time you reload Olympus, as long as the DCS mission was not restarted. +
    +
    + ); + }} > <> {appState === OlympusState.DRAW && appSubState === DrawSubState.NO_SUBSTATE && ( @@ -301,11 +349,9 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { bg-olympus-600 p-5 `} > -
    - Automatic IADS generation -
    +
    Automatic IADS generation
    {types.map((type, idx) => { if (!(type in typesSelection)) { diff --git a/frontend/react/src/ui/panels/gamemastermenu.tsx b/frontend/react/src/ui/panels/gamemastermenu.tsx index 49a22f60..a321a107 100644 --- a/frontend/react/src/ui/panels/gamemastermenu.tsx +++ b/frontend/react/src/ui/panels/gamemastermenu.tsx @@ -6,6 +6,9 @@ import { getApp } from "../../olympusapp"; import { ServerStatus } from "../../interfaces"; import { CommandModeOptionsChangedEvent, ServerStatusUpdatedEvent } from "../../events"; import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, ERAS_ORDER, GAME_MASTER, RED_COMMANDER } from "../../constants/constants"; +import { secondsToTimeString } from "../../other/utils"; +import { FaQuestionCircle } from "react-icons/fa"; +import { FaMinus, FaPlus } from "react-icons/fa6"; export function GameMasterMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS); @@ -21,53 +24,131 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil }, []); return ( - + { + return ( +
    +

    Game Master menu

    +
    + The Game Master menu allows the Game Master to set up the game session for the Real Time Strategy game mode of DCS Olympus. +
    +
    + In this mode, commanders can play against eachother in a real-time strategy game, where they can spawn a limited amount of units. Each commander can only control units belonging to their coalition. Moreover, they can only see enemy units if detected, so proper placement of radars is crucial. +
    +
    + The Game Master can set up the game session by restricting the unit spawns, setting the setup time, and restricting the eras of the units that can be spawned. Moreover, the Game Master can set the amount of spawn points available for each coalition. +
    +
    + During the setup time, commanders can prepare the battlefield. As long as they have sufficient spawn points, they can place units anywhere on the map. After the setup time ends, the game starts and the restrictions are enforced. +
    +
    + When restrictions are enforced, commanders will no longer be able to spawn ground units, and air units can only be spawned from airfields. +
    +
    + There are multiple additional modes of play. You can disable the spawn restrictions to allow commanders to spawn units freely, but can only see detected units, or you can set the spawn points to 0 to disable unit spawns entirely and force commanders to only use the units they have at the start of the game or that you provide. +
    +
    + ); + }}>
    - You are operating as: - {commandModeOptions.commandMode === GAME_MASTER && ( -
    - GAME MASTER -
    - )} - {commandModeOptions.commandMode === BLUE_COMMANDER &&
    BLUE COMMANDER
    } - {commandModeOptions.commandMode === RED_COMMANDER &&
    RED COMMANDER
    } - {serverStatus.elapsedTime > currentSetupTime && ( -
    - Setup time has ended -
    - )} - {serverStatus.elapsedTime <= currentSetupTime && ( -
    - SETUP ends in {(currentSetupTime - serverStatus.elapsedTime)?.toFixed()} seconds + {commandModeOptions.restrictSpawns ? ( + <> +
    +
    + +
    +
    + Unit spawns are restricted. During the SETUP phase, commanders can spawn units according to the settings below. After the SETUP phase ends, + ground/navy units and air spawns are disabled, and commanders can spawn aircraft/helicopters only from airfields. +
    +
    +
    + {commandModeOptions.commandMode === GAME_MASTER && ( + + )} +
    +
    + {currentSetupTime - serverStatus.elapsedTime > 0 ? ( +
    SETUP ends in {secondsToTimeString(currentSetupTime - serverStatus.elapsedTime)}
    + ) : ( +
    SETUP ended, restrictions active
    + )} +
    + {commandModeOptions.commandMode === GAME_MASTER && ( + + )} +
    + + ) : ( +
    +
    + +
    +
    + Unit spawns are NOT restricted, therefore no setup time is enforced and commanders can spawn units as desired. Only unit detection is enforced. +
    )} Options: -
    +
    { @@ -82,13 +163,13 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER} className={`data-[disabled='true']:text-gray-400`} > - Restrict unit spanws + Restrict unit spawns
    { @@ -120,7 +201,7 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil key={era} className={` group flex flex-row rounded-md justify-content - cursor-pointer gap-4 p-2 + cursor-pointer gap-4 px-2 dark:hover:bg-olympus-400 `} onClick={() => { @@ -224,56 +305,6 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil }} >
    -
    - - Setup time (seconds) - - { - if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; - const newCommandModeOptions = { ...commandModeOptions }; - newCommandModeOptions.setupTime = parseInt(e.target.value); - setCommandModeOptions(newCommandModeOptions); - }} - onIncrease={() => { - if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; - const newCommandModeOptions = { ...commandModeOptions }; - newCommandModeOptions.setupTime = Math.min(newCommandModeOptions.setupTime + 10, 6000); - setCommandModeOptions(newCommandModeOptions); - }} - onDecrease={() => { - if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; - const newCommandModeOptions = { ...commandModeOptions }; - newCommandModeOptions.setupTime = Math.max(newCommandModeOptions.setupTime - 10, 0); - setCommandModeOptions(newCommandModeOptions); - }} - > -
    -
    - Elapsed time (seconds){" "} - - {serverStatus.elapsedTime?.toFixed()} - -
    {commandModeOptions.commandMode === GAME_MASTER && (
    )} @@ -353,6 +356,9 @@ export function SpawnContextMenu(props: {}) { /> ); })} + {blueprints.length === 0 && No helicopter available}
    )} @@ -403,6 +409,9 @@ export function SpawnContextMenu(props: {}) { /> ); })} + {blueprints.length === 0 && No air defence unit available}
    )} @@ -453,6 +462,9 @@ export function SpawnContextMenu(props: {}) { /> ); })} + {blueprints.length === 0 && No ground unit available}
    )} @@ -500,10 +512,13 @@ export function SpawnContextMenu(props: {}) { /> ); })} + {blueprints.length === 0 && No navy unit available}
    )} - {openAccordion === CategoryGroup.EFFECT && ( + {openAccordion === CategoryGroup.EFFECT && commandModeOptions.commandMode === GAME_MASTER && ( <>
    )} + {openAccordion === CategoryGroup.EFFECT && commandModeOptions.commandMode !== GAME_MASTER && ( +
    + Not available in this mode +
    + )} {openAccordion === CategoryGroup.SEARCH && (
    setFilterString(value)} text={filterString} /> diff --git a/frontend/react/src/ui/panels/airbasemenu.tsx b/frontend/react/src/ui/panels/airbasemenu.tsx index 5c046cb1..5637a7fa 100644 --- a/frontend/react/src/ui/panels/airbasemenu.tsx +++ b/frontend/react/src/ui/panels/airbasemenu.tsx @@ -117,10 +117,9 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
    {Object.keys(runway.headings[0]).map((runwayName) => { return ( -
    +
    {" "} RWY {runwayName} @@ -213,6 +212,9 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre /> ); })} + {filteredBlueprints.filter((blueprint) => blueprint.category === "aircraft").length === 0 && ( + No aircraft available + )}
    void; childre /> ); })} + {filteredBlueprints.filter((blueprint) => blueprint.category === "helicopter").length === 0 && ( + No helicopter available + )}
    diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index 3437c8f5..fd9a390b 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -124,12 +124,19 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?

    Spawn menu

    The spawn menu allows you to spawn new units in the current mission.

    Moreover, it allows you to spawn effects like smokes and explosions.

    -

    You can use the search bar to quickly find a specific unit. Otherwise, open the category you are interested in, and use the filters to refine your selection.

    - -
    Click on a unit to enter the spawn properties menu. The menu is divided into multiple sections: +

    + You can use the search bar to quickly find a specific unit. Otherwise, open the category you are interested in, and use the filters to refine your + selection.{" "} +

    + +
    + Click on a unit to enter the spawn properties menu. The menu is divided into multiple sections:
    • Unit name and short description
    • Quick access name
    • @@ -139,11 +146,18 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?

    To get more info on each control, hover your cursor on it.

    Quick access

    -

    If you plan on reusing the same spawn multiple times during the mission, you can "star" the spawn properties. This will allow you to reuse them quickly multiple times. The starred spawn will save all settings, so you can create starred spawn with multiple variations, e.g. loadouts, or skill levels.

    - +

    + If you plan on reusing the same spawn multiple times during the mission, you can "star" the spawn properties. This will allow you to reuse them + quickly multiple times. The starred spawn will save all settings, so you can create starred spawn with multiple variations, e.g. loadouts, or + skill levels. +

    +
    ); }} @@ -202,6 +216,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? /> ); })} + {filteredBlueprints.filter((blueprint) => blueprint.category === "aircraft").length === 0 && ( + No aircraft available + )}
    void; children? /> ); })} + {filteredBlueprints.filter((blueprint) => blueprint.category === "helicopter").length === 0 && ( + No helicopter available + )}
    void; children? /> ); })} + {filteredBlueprints.filter((blueprint) => blueprint.category === "groundunit" && blueprint.type === "SAM Site").length === 0 && ( + No SAM sites available + )}
    void; children? /> ); })} + {filteredBlueprints.filter((blueprint) => blueprint.canAAA).length === 0 && No AAA unit available}
    void; children? /> ); })} + {filteredBlueprints.filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site").length === 0 && ( + No ground unit available + )}
    void; children? /> ); })} + {filteredBlueprints.filter((blueprint) => blueprint.category === "navyunit").length === 0 && ( + No navy unit available + )}
    - { - setOpenAccordion(openAccordion === CategoryAccordion.EFFECT ? CategoryAccordion.NONE : CategoryAccordion.EFFECT); - setSelectedRole(null); - setSelectedType(null); - }} - > -
    { + setOpenAccordion(openAccordion === CategoryAccordion.EFFECT ? CategoryAccordion.NONE : CategoryAccordion.EFFECT); + setSelectedRole(null); + setSelectedType(null); + }} > - { - setEffect("explosion"); - }} - /> - { - setEffect("smoke"); - }} - /> -
    -
    +
    + { + setEffect("explosion"); + }} + /> + { + setEffect("smoke"); + }} + /> +
    + + )}
    )} diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index 39003f66..99ce1c2c 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -13,7 +13,7 @@ import { msToKnots, } from "../other/utils"; import { CoalitionPolygon } from "../map/coalitionarea/coalitionpolygon"; -import { DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, OlympusState, UnitControlSubState } from "../constants/constants"; +import { BLUE_COMMANDER, DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, OlympusState, RED_COMMANDER, UnitControlSubState } from "../constants/constants"; import { DataExtractor } from "../server/dataextractor"; import { citiesDatabase } from "./databases/citiesdatabase"; import { TemporaryUnitMarker } from "../map/markers/temporaryunitmarker"; @@ -39,6 +39,7 @@ import { import { UnitDatabase } from "./databases/unitdatabase"; import * as turf from "@turf/turf"; import { PathMarker } from "../map/markers/pathmarker"; +import { Coalition } from "../types/types"; /** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only * result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows @@ -1257,12 +1258,20 @@ export class UnitsManager { if (units === null) units = this.getSelectedUnits(); units = units.filter((unit) => !unit.getHuman()); + // TODO: maybe check units are all of same coalition? + let callback = (units) => { onExecution(); if (this.getUnitsCategories(units).length == 1) { var unitsData: { ID: number; location: LatLng }[] = []; units.forEach((unit: Unit) => unitsData.push({ ID: unit.ID, location: unit.getPosition() })); - getApp().getServerManager().cloneUnits(unitsData, true, 0 /* No spawn points, we delete the original units */); + + /* Determine the coalition */ + let coalition = "all"; + if (getApp().getMissionManager().getCommandModeOptions().commandMode === BLUE_COMMANDER) coalition = "blue"; + else if (getApp().getMissionManager().getCommandModeOptions().commandMode === RED_COMMANDER) coalition = "red"; + + getApp().getServerManager().cloneUnits(unitsData, true, 0 /* No spawn points, we delete the original units */, coalition as Coalition); this.#showActionMessage(units, `created a group`); } else { getApp().addInfoMessage(`Groups can only be created from units of the same category`); @@ -1469,9 +1478,13 @@ export class UnitsManager { units.push({ ID: unit.ID, location: position }); }); + let coalition = "all"; + if (getApp().getMissionManager().getCommandModeOptions().commandMode === BLUE_COMMANDER) coalition = "blue"; + else if (getApp().getMissionManager().getCommandModeOptions().commandMode === RED_COMMANDER) coalition = "red"; + getApp() .getServerManager() - .cloneUnits(units, false, spawnPoints, (res: any) => { + .cloneUnits(units, false, getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: spawnPoints, coalition as Coalition, (res: any) => { if (res !== undefined) { markers.forEach((marker: TemporaryUnitMarker) => { marker.setCommandHash(res); @@ -1655,7 +1668,7 @@ export class UnitsManager { getApp().addInfoMessage("Aircrafts can be air spawned during the SETUP phase only"); return false; } - spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { + spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => { return points + this.getDatabase().getSpawnPointsByName(unit.unitType); }, 0); spawnFunction = () => getApp().getServerManager().spawnAircrafts(units, coalition, airbase, country, immediate, spawnPoints, callback); @@ -1664,7 +1677,7 @@ export class UnitsManager { getApp().addInfoMessage("Helicopters can be air spawned during the SETUP phase only"); return false; } - spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { + spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => { return points + this.getDatabase().getSpawnPointsByName(unit.unitType); }, 0); spawnFunction = () => getApp().getServerManager().spawnHelicopters(units, coalition, airbase, country, immediate, spawnPoints, callback); @@ -1673,7 +1686,7 @@ export class UnitsManager { getApp().addInfoMessage("Ground units can be spawned during the SETUP phase only"); return false; } - spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { + spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => { return points + this.getDatabase().getSpawnPointsByName(unit.unitType); }, 0); spawnFunction = () => getApp().getServerManager().spawnGroundUnits(units, coalition, country, immediate, spawnPoints, callback); @@ -1682,7 +1695,7 @@ export class UnitsManager { getApp().addInfoMessage("Navy units can be spawned during the SETUP phase only"); return false; } - spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { + spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => { return points + this.getDatabase().getSpawnPointsByName(unit.unitType); }, 0); spawnFunction = () => getApp().getServerManager().spawnNavyUnits(units, coalition, country, immediate, spawnPoints, callback); From 99e742498df6d76e317debced39297ef723209df Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Thu, 20 Mar 2025 19:30:40 +0100 Subject: [PATCH 33/33] fix: error in spawn menu, fixed smoke not working --- .../src/ui/contextmenus/spawncontextmenu.tsx | 10 +++++----- .../react/src/ui/panels/effectspawnmenu.tsx | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx b/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx index f5e02fbe..3d7ff8d5 100644 --- a/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx +++ b/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx @@ -306,7 +306,7 @@ export function SpawnContextMenu(props: {}) { /> ); })} - {blueprints.length === 0 && No aircraft available}
    @@ -356,7 +356,7 @@ export function SpawnContextMenu(props: {}) { /> ); })} - {blueprints.length === 0 && No helicopter available}
    @@ -409,7 +409,7 @@ export function SpawnContextMenu(props: {}) { /> ); })} - {blueprints.length === 0 && No air defence unit available}
    @@ -462,7 +462,7 @@ export function SpawnContextMenu(props: {}) { /> ); })} - {blueprints.length === 0 && No ground unit available}
    @@ -512,7 +512,7 @@ export function SpawnContextMenu(props: {}) { /> ); })} - {blueprints.length === 0 && No navy unit available}
    diff --git a/frontend/react/src/ui/panels/effectspawnmenu.tsx b/frontend/react/src/ui/panels/effectspawnmenu.tsx index b86fb956..d449b250 100644 --- a/frontend/react/src/ui/panels/effectspawnmenu.tsx +++ b/frontend/react/src/ui/panels/effectspawnmenu.tsx @@ -30,11 +30,17 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff type: props.effect, explosionType, }); - else if (props.effect === "smoke") + else if (props.effect === "smoke") { + let colorName = "white"; + if (smokeColor === colors.BLUE) colorName = "blue"; + else if (smokeColor === colors.RED) colorName = "red"; + else if (smokeColor === colors.GREEN) colorName = "green"; + else if (smokeColor === colors.ORANGE) colorName = "orange"; getApp()?.getMap()?.setEffectRequestTable({ type: props.effect, - smokeColor, + smokeColor: colorName, }); + } getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_EFFECT); } else { if (appState === OlympusState.SPAWN && appSubState === SpawnSubState.SPAWN_EFFECT) getApp().setState(OlympusState.IDLE); @@ -129,7 +135,13 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff else if (explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", props.latlng); getApp().getMap().addExplosionMarker(props.latlng); } else if (props.effect === "smoke") { - getApp().getServerManager().spawnSmoke(smokeColor, props.latlng); + /* Find the name of the color */ + let colorName = "white"; + if (smokeColor === colors.BLUE) colorName = "blue"; + else if (smokeColor === colors.RED) colorName = "red"; + else if (smokeColor === colors.GREEN) colorName = "green"; + else if (smokeColor === colors.ORANGE) colorName = "orange"; + getApp().getServerManager().spawnSmoke(colorName, props.latlng); getApp() .getMap() .addSmokeMarker(props.latlng, smokeColor ?? colors.WHITE);