From 34f9a8bc40a83a0252b786f2286aa2ff1d10ed83 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Wed, 12 Mar 2025 18:43:14 +0100 Subject: [PATCH] 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"