From a257afca4ba54f933f4936b2bea5eb2b2aaef1d3 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Sat, 27 Sep 2025 18:07:37 +0200 Subject: [PATCH] Add customString and customInteger to Unit data model Introduced customString and customInteger fields to the Unit class in both backend (C++) and frontend (TypeScript/React). Updated data indexes, interfaces, and API handling to support setting and retrieving these custom fields. Also added UI elements in the unit control menu to display and handle these new properties. --- backend/core/include/datatypes.h | 2 + backend/core/include/unit.h | 7 + backend/core/src/scheduler.cpp | 47 +- backend/core/src/unit.cpp | 2 + frontend/react/src/constants/constants.ts | 2 + frontend/react/src/interfaces.ts | 2 + .../react/src/ui/panels/unitcontrolmenu.tsx | 5557 ++++++++++------- frontend/react/src/unit/unit.ts | 16 + scripts/python/API/.vscode/launch.json | 8 + scripts/python/API/data/data_indexes.py | 2 + scripts/python/API/infantry_boarding.py | 625 ++ scripts/python/API/olympus.json | 2 +- scripts/python/API/unit/unit.py | 24 +- 13 files changed, 3946 insertions(+), 2350 deletions(-) create mode 100644 scripts/python/API/infantry_boarding.py diff --git a/backend/core/include/datatypes.h b/backend/core/include/datatypes.h index fb52e979..0ce20be1 100644 --- a/backend/core/include/datatypes.h +++ b/backend/core/include/datatypes.h @@ -72,6 +72,8 @@ namespace DataIndex { airborne, cargoWeight, drawArguments, + customString, + customInteger, lastIndex, endOfData = 255 }; diff --git a/backend/core/include/unit.h b/backend/core/include/unit.h index fad95eee..bbffadfb 100644 --- a/backend/core/include/unit.h +++ b/backend/core/include/unit.h @@ -132,6 +132,8 @@ public: virtual void setAirborne(bool newValue) { updateValue(airborne, newValue, DataIndex::airborne); } virtual void setCargoWeight(double newValue) { updateValue(cargoWeight, newValue, DataIndex::cargoWeight); } virtual void setDrawArguments(vector newValue); + virtual void setCustomString(string newValue) { updateValue(customString, newValue, DataIndex::customString); } + virtual void setCustomInteger(unsigned long newValue) { updateValue(customInteger, newValue, DataIndex::customInteger); } /********** Getters **********/ virtual string getCategory() { return category; } @@ -201,6 +203,8 @@ public: virtual bool getAirborne() { return airborne; } virtual double getCargoWeight() { return cargoWeight; } virtual vector getDrawArguments() { return drawArguments; } + virtual string getCustomString() { return customString; } + virtual unsigned long getCustomInteger() { return customInteger; } protected: unsigned int ID; @@ -273,6 +277,9 @@ protected: bool airborne = false; double cargoWeight = 0; vector drawArguments; + + string customString = ""; + unsigned long customInteger = 0; /********** Other **********/ unsigned int taskCheckCounter = 0; diff --git a/backend/core/src/scheduler.cpp b/backend/core/src/scheduler.cpp index 8d17619d..67393614 100644 --- a/backend/core/src/scheduler.cpp +++ b/backend/core/src/scheduler.cpp @@ -189,7 +189,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js string color = to_string(value[L"color"]); 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; command = dynamic_cast(new Smoke(color, loc)); log(username + " added a " + color + " smoke at (" + to_string(lat) + ", " + to_string(lng) + ")", true); @@ -223,8 +223,8 @@ void Scheduler::handleRequest(string key, json::value value, string username, js string liveryID = to_string(unit[L"liveryID"]); string skill = to_string(unit[L"skill"]); - spawnOptions.push_back({unitType, location, loadout, skill, liveryID, heading}); - log(username + " spawned a " + coalition + " " + unitType , true); + spawnOptions.push_back({ unitType, location, loadout, skill, liveryID, heading }); + log(username + " spawned a " + coalition + " " + unitType, true); } if (key.compare("spawnAircrafts") == 0) @@ -257,8 +257,8 @@ void Scheduler::handleRequest(string key, json::value value, string username, js Coords location; location.lat = lat; location.lng = lng; string liveryID = to_string(unit[L"liveryID"]); string skill = to_string(unit[L"skill"]); - - spawnOptions.push_back({ unitType, location, "", skill, liveryID, heading}); + + spawnOptions.push_back({ unitType, location, "", skill, liveryID, heading }); log(username + " spawned a " + coalition + " " + unitType, true); } @@ -404,7 +404,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js unsigned int ID = unit[L"ID"].as_integer(); double lat = unit[L"location"][L"lat"].as_double(); double lng = unit[L"location"][L"lng"].as_double(); - + Coords location; location.lat = lat; location.lng = lng; cloneOptions.push_back({ ID, location }); log(username + " cloning unit with ID " + to_string(ID), true); @@ -433,7 +433,8 @@ void Scheduler::handleRequest(string key, json::value value, string username, js unsigned char alarmState = value[L"alarmState"].as_number().to_uint32(); unit->setAlarmState(alarmState); log(username + " set unit " + unit->getUnitName() + "(" + unit->getName() + ") alarm state to " + to_string(alarmState), true); - } else { + } + else { log("Error while setting setAlarmState. Unit does not exist."); } } @@ -562,7 +563,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js 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); } } @@ -587,7 +588,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js Unit* unit = unitsManager->getGroupLeader(ID); if (unit != nullptr) { unit->setOnOff(onOff); - log(username + " set unit " + unit->getUnitName() + "(" + unit->getName() + ") onOff to: " + (onOff? "true": "false"), true); + log(username + " set unit " + unit->getUnitName() + "(" + unit->getName() + ") onOff to: " + (onOff ? "true" : "false"), true); } } /************************/ @@ -711,7 +712,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js unitsManager->acquireControl(ID); unsigned char operateAs = value[L"operateAs"].as_number().to_uint32(); Unit* unit = unitsManager->getGroupLeader(ID); - if (unit != nullptr) + if (unit != nullptr) unit->setOperateAs(operateAs); } /************************/ @@ -817,7 +818,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js command = dynamic_cast(new DeleteSpot(spotID)); } /************************/ - else if (key.compare("setCommandModeOptions") == 0) + else if (key.compare("setCommandModeOptions") == 0) { setCommandModeOptions(value); log(username + " updated the Command Mode Options", true); @@ -827,7 +828,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js unitsManager->loadDatabases(); } /************************/ - else if (key.compare("setCargoWeight") == 0) + else if (key.compare("setCargoWeight") == 0) { unsigned int ID = value[L"ID"].as_integer(); Unit* unit = unitsManager->getUnit(ID); @@ -852,6 +853,28 @@ void Scheduler::handleRequest(string key, json::value value, string username, js } } /************************/ + else if (key.compare("setCustomString") == 0) + { + unsigned int ID = value[L"ID"].as_integer(); + Unit* unit = unitsManager->getUnit(ID); + if (unit != nullptr) { + string customString = to_string(value[L"customString"]); + unit->setCustomString(customString); + log(username + " set custom string to unit " + unit->getUnitName() + "(" + unit->getName() + "), " + customString, true); + } + } + /************************/ + else if (key.compare("setCustomInteger") == 0) + { + unsigned int ID = value[L"ID"].as_integer(); + Unit* unit = unitsManager->getUnit(ID); + if (unit != nullptr) { + double customNumber = value[L"customInteger"].as_double(); + unit->setCustomInteger(customNumber); + log(username + " set custom number to unit " + unit->getUnitName() + "(" + unit->getName() + "), " + to_string(customNumber), true); + } + } + /************************/ else { log("Unknown command: " + key); diff --git a/backend/core/src/unit.cpp b/backend/core/src/unit.cpp index 8392f542..220465d8 100644 --- a/backend/core/src/unit.cpp +++ b/backend/core/src/unit.cpp @@ -344,6 +344,8 @@ void Unit::getData(stringstream& ss, unsigned long long time) case DataIndex::airborne: appendNumeric(ss, datumIndex, airborne); break; case DataIndex::cargoWeight: appendNumeric(ss, datumIndex, cargoWeight); break; case DataIndex::drawArguments: appendVector(ss, datumIndex, drawArguments); break; + case DataIndex::customString: appendString(ss, datumIndex, customString); break; + case DataIndex::customInteger: appendNumeric(ss, datumIndex, customInteger); break; } } } diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index e95c7267..28de5ad2 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -549,6 +549,8 @@ export enum DataIndexes { airborne, cargoWeight, drawingArguments, + customString, + customInteger, endOfData = 255, } diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index f32c5bb8..2abaf0b5 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -293,6 +293,8 @@ export interface UnitData { airborne: boolean; cargoWeight: number; drawingArguments: DrawingArgument[]; + customString: string; + customInteger: number; } export interface LoadoutItemBlueprint { diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 4dc15ac1..776069cb 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -7,48 +7,48 @@ import { getApp } from "../../olympusapp"; import { OlButtonGroup, OlButtonGroupItem } from "../components/olbuttongroup"; import { OlCheckbox } from "../components/olcheckbox"; import { - AudioManagerState, - ROEs, - UnitState, - altitudeIncrements, - emissionsCountermeasures, - maxAltitudeValues, - maxSpeedValues, - reactionsToThreat, - speedIncrements, + AudioManagerState, + ROEs, + UnitState, + altitudeIncrements, + emissionsCountermeasures, + maxAltitudeValues, + maxSpeedValues, + reactionsToThreat, + speedIncrements, } from "../../constants/constants"; import { OlToggle } from "../components/oltoggle"; import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; import { - olButtonsAlarmstateAuto, - olButtonsAlarmstateGreen, - olButtonsAlarmstateRed, - olButtonsEmissionsAttack, - olButtonsEmissionsDefend, - olButtonsEmissionsFree, - olButtonsEmissionsSilent, - olButtonsIntensity1, - olButtonsIntensity2, - olButtonsIntensity3, - olButtonsRoeDesignated, - olButtonsRoeFree, - olButtonsRoeHold, - olButtonsRoeReturn, - olButtonsScatter1, - olButtonsScatter2, - olButtonsScatter3, - olButtonsThreatEvade, - olButtonsThreatManoeuvre, - olButtonsThreatNone, - olButtonsThreatPassive, - olButtonsVisibilityAircraft, - olButtonsVisibilityDcs, - olButtonsVisibilityGroundunit, - olButtonsVisibilityGroundunitSam, - olButtonsVisibilityHelicopter, - olButtonsVisibilityHuman, - olButtonsVisibilityNavyunit, - olButtonsVisibilityOlympus, + olButtonsAlarmstateAuto, + olButtonsAlarmstateGreen, + olButtonsAlarmstateRed, + olButtonsEmissionsAttack, + olButtonsEmissionsDefend, + olButtonsEmissionsFree, + olButtonsEmissionsSilent, + olButtonsIntensity1, + olButtonsIntensity2, + olButtonsIntensity3, + olButtonsRoeDesignated, + olButtonsRoeFree, + olButtonsRoeHold, + olButtonsRoeReturn, + olButtonsScatter1, + olButtonsScatter2, + olButtonsScatter3, + olButtonsThreatEvade, + olButtonsThreatManoeuvre, + olButtonsThreatNone, + olButtonsThreatPassive, + olButtonsVisibilityAircraft, + olButtonsVisibilityDcs, + olButtonsVisibilityGroundunit, + olButtonsVisibilityGroundunitSam, + olButtonsVisibilityHelicopter, + olButtonsVisibilityHuman, + olButtonsVisibilityNavyunit, + olButtonsVisibilityOlympus, } from "../components/olicons"; import { Coalition } from "../../types/types"; import { convertROE, deepCopyTable, ftToM, knotsToMs, mToFt, msToKnots, zeroAppend } from "../../other/utils"; @@ -69,1454 +69,1890 @@ import { OlLocation } from "../components/ollocation"; import { OlStateButton } from "../components/olstatebutton"; export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { - function initializeUnitsData() { - return { - desiredAltitude: undefined as undefined | number, - desiredAltitudeType: undefined as undefined | string, - desiredSpeed: undefined as undefined | number, - desiredSpeedType: undefined as undefined | string, - ROE: undefined as undefined | string, - reactionToThreat: undefined as undefined | string, - emissionsCountermeasures: undefined as undefined | string, - scenicAAA: undefined as undefined | boolean, - missOnPurpose: undefined as undefined | boolean, - shotsScatter: undefined as undefined | number, - shotsIntensity: undefined as undefined | number, - operateAs: undefined as undefined | Coalition, - followRoads: undefined as undefined | boolean, - isActiveAWACS: undefined as undefined | boolean, - isActiveTanker: undefined as undefined | boolean, - onOff: undefined as undefined | boolean, - isAudioSink: undefined as undefined | boolean, - radio: undefined as undefined | Radio, - TACAN: undefined as undefined | TACAN, - generalSettings: undefined as undefined | GeneralSettings, - alarmState: undefined as undefined | AlarmState, - }; - } - - const [selectedUnits, setSelectedUnits] = useState([] as Unit[]); - const [audioManagerRunning, setAudioManagerRunning] = useState(false); - const [selectedUnitsData, setSelectedUnitsData] = useState(initializeUnitsData); - const [forcedUnitsData, setForcedUnitsData] = useState(initializeUnitsData); - const [selectionFilter, setSelectionFilter] = useState({ - control: { - human: true, - dcs: true, - olympus: true, - }, - blue: { - aircraft: true, - helicopter: true, - "groundunit-sam": true, - groundunit: true, - navyunit: true, - }, - neutral: { - aircraft: true, - helicopter: true, - "groundunit-sam": true, - groundunit: true, - navyunit: true, - }, - red: { - aircraft: true, - helicopter: true, - "groundunit-sam": true, - groundunit: true, - navyunit: true, - }, - }); - const [selectionID, setSelectionID] = useState(null as null | number); - const [searchBarRefState, setSearchBarRefState] = useState(null as MutableRefObject | null); - const [filterString, setFilterString] = useState(""); - const [showRadioSettings, setShowRadioSettings] = useState(false); - const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); - const [activeRadioSettings, setActiveRadioSettings] = useState(null as null | { radio: Radio; TACAN: TACAN }); - const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | GeneralSettings); - const [lastUpdateTime, setLastUpdateTime] = useState(0); - const [showScenicModes, setShowScenicModes] = useState(false); - const [showEngagementSettings, setShowEngagementSettings] = useState(false); - const [barrelHeight, setBarrelHeight] = useState(0); - const [muzzleVelocity, setMuzzleVelocity] = useState(0); - const [aimTime, setAimTime] = useState(0); - const [shotsToFire, setShotsToFire] = useState(0); - const [shotsBaseInterval, setShotsBaseInterval] = useState(0); - const [shotsBaseScatter, setShotsBaseScatter] = useState(0); - const [engagementRange, setEngagementRange] = useState(0); - const [targetingRange, setTargetingRange] = useState(0); - const [aimMethodRange, setAimMethodRange] = useState(0); - const [acquisitionRange, setAcquisitionRange] = useState(0); - - var searchBarRef = useRef(null); - - useEffect(() => { - SelectedUnitsChangedEvent.on((units) => setSelectedUnits(units)); - SelectionClearedEvent.on(() => setSelectedUnits([])); - AudioManagerStateChangedEvent.on((state) => setAudioManagerRunning(state === AudioManagerState.RUNNING)); - UnitsUpdatedEvent.on((units) => units.find((unit) => unit.getSelected()) && setLastUpdateTime(Date.now())); - }, []); - - useEffect(() => { - if (!searchBarRefState) setSearchBarRefState(searchBarRef); - if (!props.open && selectionID !== null) setSelectionID(null); - if (!props.open && filterString !== "") setFilterString(""); - }); - - const updateData = useCallback(() => { - const getters = { - desiredAltitude: (unit: Unit) => Math.round(mToFt(unit.getDesiredAltitude())), - desiredAltitudeType: (unit: Unit) => unit.getDesiredAltitudeType(), - desiredSpeed: (unit: Unit) => Math.round(msToKnots(unit.getDesiredSpeed())), - desiredSpeedType: (unit: Unit) => unit.getDesiredSpeedType(), - ROE: (unit: Unit) => unit.getROE(), - reactionToThreat: (unit: Unit) => unit.getReactionToThreat(), - emissionsCountermeasures: (unit: Unit) => unit.getEmissionsCountermeasures(), - scenicAAA: (unit: Unit) => unit.getState() === "scenic-aaa", - missOnPurpose: (unit: Unit) => unit.getState() === "miss-on-purpose", - shotsScatter: (unit: Unit) => unit.getShotsScatter(), - shotsIntensity: (unit: Unit) => unit.getShotsIntensity(), - operateAs: (unit: Unit) => unit.getOperateAs(), - followRoads: (unit: Unit) => unit.getFollowRoads(), - isActiveAWACS: (unit: Unit) => unit.getIsActiveAWACS(), - isActiveTanker: (unit: Unit) => unit.getIsActiveTanker(), - onOff: (unit: Unit) => unit.getOnOff(), - radio: (unit: Unit) => unit.getRadio(), - TACAN: (unit: Unit) => unit.getTACAN(), - alarmState: (unit: Unit) => unit.getAlarmState(), - generalSettings: (unit: Unit) => unit.getGeneralSettings(), - isAudioSink: (unit: Unit) => { - return ( - getApp() - ?.getAudioManager() - .getSinks() - .filter((sink) => { - return sink instanceof UnitSink; - }).length > 0 && - getApp() - ?.getAudioManager() - .getSinks() - .find((sink) => { - return sink instanceof UnitSink && sink.getUnit() === unit; - }) !== undefined - ); - }, - } as { [key in keyof typeof selectedUnitsData]: (unit: Unit) => void }; - - var updatedData = {}; - let anyForcedDataUpdated = false; - Object.entries(getters).forEach(([key, getter]) => { - let newDatum = getApp()?.getUnitsManager()?.getSelectedUnitsVariable(getter); - if (forcedUnitsData[key] !== undefined) { - if (newDatum === forcedUnitsData[key]) { - anyForcedDataUpdated = true; - forcedUnitsData[key] = undefined; - } else updatedData[key] = forcedUnitsData[key]; - } else updatedData[key] = newDatum; - }); - setSelectedUnitsData(updatedData as typeof selectedUnitsData); - if (anyForcedDataUpdated) setForcedUnitsData({ ...forcedUnitsData }); - }, [forcedUnitsData]); - useEffect(updateData, [selectedUnits, lastUpdateTime, forcedUnitsData]); - - useEffect(() => { - setForcedUnitsData(initializeUnitsData); - setShowRadioSettings(false); - setShowAdvancedSettings(false); - - if (selectedUnits.length > 0) { - setBarrelHeight(selectedUnits[0].getBarrelHeight()); - setMuzzleVelocity(selectedUnits[0].getMuzzleVelocity()); - setAimTime(selectedUnits[0].getAimTime()); - setShotsToFire(selectedUnits[0].getShotsToFire()); - setShotsBaseInterval(selectedUnits[0].getShotsBaseInterval()); - setShotsBaseScatter(selectedUnits[0].getShotsBaseScatter()); - setEngagementRange(selectedUnits[0].getEngagementRange()); - setTargetingRange(selectedUnits[0].getTargetingRange()); - setAimMethodRange(selectedUnits[0].getAimMethodRange()); - setAcquisitionRange(selectedUnits[0].getAcquisitionRange()); + function initializeUnitsData() { + return { + desiredAltitude: undefined as undefined | number, + desiredAltitudeType: undefined as undefined | string, + desiredSpeed: undefined as undefined | number, + desiredSpeedType: undefined as undefined | string, + ROE: undefined as undefined | string, + reactionToThreat: undefined as undefined | string, + emissionsCountermeasures: undefined as undefined | string, + scenicAAA: undefined as undefined | boolean, + missOnPurpose: undefined as undefined | boolean, + shotsScatter: undefined as undefined | number, + shotsIntensity: undefined as undefined | number, + operateAs: undefined as undefined | Coalition, + followRoads: undefined as undefined | boolean, + isActiveAWACS: undefined as undefined | boolean, + isActiveTanker: undefined as undefined | boolean, + onOff: undefined as undefined | boolean, + isAudioSink: undefined as undefined | boolean, + radio: undefined as undefined | Radio, + TACAN: undefined as undefined | TACAN, + generalSettings: undefined as undefined | GeneralSettings, + alarmState: undefined as undefined | AlarmState, + }; } - }, [selectedUnits]); - /* Count how many units are selected of each type, divided by coalition */ - var unitOccurences: { - blue: { [key: string]: { label: string; occurences: number } }; - red: { [key: string]: { label: string; occurences: number } }; - neutral: { [key: string]: { label: string; occurences: number } }; - } = { - blue: {}, - red: {}, - neutral: {}, - }; + const [selectedUnits, setSelectedUnits] = useState([] as Unit[]); + const [audioManagerRunning, setAudioManagerRunning] = useState(false); + const [selectedUnitsData, setSelectedUnitsData] = useState(initializeUnitsData); + const [forcedUnitsData, setForcedUnitsData] = useState(initializeUnitsData); + const [selectionFilter, setSelectionFilter] = useState({ + control: { + human: true, + dcs: true, + olympus: true, + }, + blue: { + aircraft: true, + helicopter: true, + "groundunit-sam": true, + groundunit: true, + navyunit: true, + }, + neutral: { + aircraft: true, + helicopter: true, + "groundunit-sam": true, + groundunit: true, + navyunit: true, + }, + red: { + aircraft: true, + helicopter: true, + "groundunit-sam": true, + groundunit: true, + navyunit: true, + }, + }); + const [selectionID, setSelectionID] = useState(null as null | number); + const [searchBarRefState, setSearchBarRefState] = useState(null as MutableRefObject | null); + const [filterString, setFilterString] = useState(""); + const [showRadioSettings, setShowRadioSettings] = useState(false); + const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); + const [activeRadioSettings, setActiveRadioSettings] = useState(null as null | { radio: Radio; TACAN: TACAN }); + const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | GeneralSettings); + const [lastUpdateTime, setLastUpdateTime] = useState(0); + const [showScenicModes, setShowScenicModes] = useState(false); + const [showEngagementSettings, setShowEngagementSettings] = useState(false); + const [barrelHeight, setBarrelHeight] = useState(0); + const [muzzleVelocity, setMuzzleVelocity] = useState(0); + const [aimTime, setAimTime] = useState(0); + const [shotsToFire, setShotsToFire] = useState(0); + const [shotsBaseInterval, setShotsBaseInterval] = useState(0); + const [shotsBaseScatter, setShotsBaseScatter] = useState(0); + const [engagementRange, setEngagementRange] = useState(0); + const [targetingRange, setTargetingRange] = useState(0); + const [aimMethodRange, setAimMethodRange] = useState(0); + const [acquisitionRange, setAcquisitionRange] = useState(0); - selectedUnits.forEach((unit) => { - if (!(unit.getName() in unitOccurences[unit.getCoalition()])) - unitOccurences[unit.getCoalition()][unit.getName()] = { occurences: 1, label: unit.getBlueprint()?.label }; - else unitOccurences[unit.getCoalition()][unit.getName()].occurences++; - }); + var searchBarRef = useRef(null); - const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? []; + useEffect(() => { + SelectedUnitsChangedEvent.on((units) => setSelectedUnits(units)); + SelectionClearedEvent.on(() => setSelectedUnits([])); + AudioManagerStateChangedEvent.on((state) => setAudioManagerRunning(state === AudioManagerState.RUNNING)); + UnitsUpdatedEvent.on((units) => units.find((unit) => unit.getSelected()) && setLastUpdateTime(Date.now())); + }, []); - const filteredUnits = Object.values(getApp()?.getUnitsManager()?.getUnits() ?? {}).filter( - (unit) => - unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0 || - (unit.getBlueprint()?.label ?? "").toLowerCase()?.indexOf(filterString.toLowerCase()) >= 0 - ); + useEffect(() => { + if (!searchBarRefState) setSearchBarRefState(searchBarRef); + if (!props.open && selectionID !== null) setSelectionID(null); + if (!props.open && filterString !== "") setFilterString(""); + }); - const everyUnitIsGround = selectedCategories.every((category) => { - return category === "GroundUnit"; - }); - const everyUnitIsNavy = selectedCategories.every((category) => { - return category === "NavyUnit"; - }); - const everyUnitIsHelicopter = selectedCategories.every((category) => { - return category === "Helicopter"; - }); + const updateData = useCallback(() => { + const getters = { + desiredAltitude: (unit: Unit) => Math.round(mToFt(unit.getDesiredAltitude())), + desiredAltitudeType: (unit: Unit) => unit.getDesiredAltitudeType(), + desiredSpeed: (unit: Unit) => Math.round(msToKnots(unit.getDesiredSpeed())), + desiredSpeedType: (unit: Unit) => unit.getDesiredSpeedType(), + ROE: (unit: Unit) => unit.getROE(), + reactionToThreat: (unit: Unit) => unit.getReactionToThreat(), + emissionsCountermeasures: (unit: Unit) => unit.getEmissionsCountermeasures(), + scenicAAA: (unit: Unit) => unit.getState() === "scenic-aaa", + missOnPurpose: (unit: Unit) => unit.getState() === "miss-on-purpose", + shotsScatter: (unit: Unit) => unit.getShotsScatter(), + shotsIntensity: (unit: Unit) => unit.getShotsIntensity(), + operateAs: (unit: Unit) => unit.getOperateAs(), + followRoads: (unit: Unit) => unit.getFollowRoads(), + isActiveAWACS: (unit: Unit) => unit.getIsActiveAWACS(), + isActiveTanker: (unit: Unit) => unit.getIsActiveTanker(), + onOff: (unit: Unit) => unit.getOnOff(), + radio: (unit: Unit) => unit.getRadio(), + TACAN: (unit: Unit) => unit.getTACAN(), + alarmState: (unit: Unit) => unit.getAlarmState(), + generalSettings: (unit: Unit) => unit.getGeneralSettings(), + isAudioSink: (unit: Unit) => { + return ( + getApp() + ?.getAudioManager() + .getSinks() + .filter((sink) => { + return sink instanceof UnitSink; + }).length > 0 && + getApp() + ?.getAudioManager() + .getSinks() + .find((sink) => { + return sink instanceof UnitSink && sink.getUnit() === unit; + }) !== undefined + ); + }, + } as { [key in keyof typeof selectedUnitsData]: (unit: Unit) => void }; - /* Speed/altitude increments */ - const minAltitude = 0; - const minSpeed = 0; + var updatedData = {}; + let anyForcedDataUpdated = false; + Object.entries(getters).forEach(([key, getter]) => { + let newDatum = getApp()?.getUnitsManager()?.getSelectedUnitsVariable(getter); + if (forcedUnitsData[key] !== undefined) { + if (newDatum === forcedUnitsData[key]) { + anyForcedDataUpdated = true; + forcedUnitsData[key] = undefined; + } else updatedData[key] = forcedUnitsData[key]; + } else updatedData[key] = newDatum; + }); + setSelectedUnitsData(updatedData as typeof selectedUnitsData); + if (anyForcedDataUpdated) setForcedUnitsData({ ...forcedUnitsData }); + }, [forcedUnitsData]); + useEffect(updateData, [selectedUnits, lastUpdateTime, forcedUnitsData]); - let maxAltitude = maxAltitudeValues.aircraft; - let maxSpeed = maxSpeedValues.aircraft; - let speedStep = speedIncrements.aircraft; - let altitudeStep = altitudeIncrements.aircraft; + useEffect(() => { + setForcedUnitsData(initializeUnitsData); + setShowRadioSettings(false); + setShowAdvancedSettings(false); - if (everyUnitIsHelicopter) { - maxAltitude = maxAltitudeValues.helicopter; - maxSpeed = maxSpeedValues.helicopter; - speedStep = speedIncrements.helicopter; - altitudeStep = altitudeIncrements.helicopter; - } else if (everyUnitIsGround) { - maxSpeed = maxSpeedValues.groundunit; - speedStep = speedIncrements.groundunit; - } else if (everyUnitIsNavy) { - maxSpeed = maxSpeedValues.navyunit; - speedStep = speedIncrements.navyunit; - } + if (selectedUnits.length > 0) { + setBarrelHeight(selectedUnits[0].getBarrelHeight()); + setMuzzleVelocity(selectedUnits[0].getMuzzleVelocity()); + setAimTime(selectedUnits[0].getAimTime()); + setShotsToFire(selectedUnits[0].getShotsToFire()); + setShotsBaseInterval(selectedUnits[0].getShotsBaseInterval()); + setShotsBaseScatter(selectedUnits[0].getShotsBaseScatter()); + setEngagementRange(selectedUnits[0].getEngagementRange()); + setTargetingRange(selectedUnits[0].getTargetingRange()); + setAimMethodRange(selectedUnits[0].getAimMethodRange()); + setAcquisitionRange(selectedUnits[0].getAcquisitionRange()); + } + }, [selectedUnits]); - return ( - 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`} - onClose={props.onClose} - autohide={true} - wiki={() => { - return ( -
-

Unit selection tool

-
- The unit control menu serves two purposes. If no unit is currently selected, it allows you to select units based on their category, coalition, and - control mode. You can also select units based on their specific type by using the search input. -
-

Unit control tool

-
- If units are selected, the menu will display the selected units and allow you to control their altitude, speed, rules of engagement, and other - settings. -
-
- The available controls depend on what type of unit is selected. Only controls applicable to every selected unit will be displayed, so make sure to - refine your selection.{" "} -
-
- {" "} - You will be able to inspect the current values of the controls, e.g. the desired altitude, rules of engagement and so on. However, if multiple - units are selected, you will only see the values of controls that are set to be the same for each selected unit. -
-
- {" "} - For example, if two airplanes are selected and they both have been instructed to fly at 1000ft, you will see the altitude slider set at that - value. But if one airplane is set to fly at 1000ft and the other at 2000ft, you will see the slider display 'Different values'. -
-
If at that point you move the slider, you will instruct both airplanes to fly at the same altitude.
-
- {" "} - If a single unit is selected, you will also be able to see additional info on the unit, like its fuel level, position and altitude, tasking, and - available ammunition.{" "} -
-
- ); - }} - > - <> - {/* ============== Selection tool START ============== */} - {selectedUnits.length == 0 && ( -
-
Selection tool
-
-
- -
-
- The selection tools allows you to select units depending on their category, coalition, and control mode. You can also select units depending on - their specific type by using the search input. -
-
-
- {selectionID === null && ( - <> -
- Control mode -
+ /* Count how many units are selected of each type, divided by coalition */ + var unitOccurences: { + blue: { [key: string]: { label: string; occurences: number } }; + red: { [key: string]: { label: string; occurences: number } }; + neutral: { [key: string]: { label: string; occurences: number } }; + } = { + blue: {}, + red: {}, + neutral: {}, + }; -
- {Object.entries({ - human: ["Human", olButtonsVisibilityHuman], - olympus: ["Olympus controlled", olButtonsVisibilityOlympus], - dcs: ["From DCS mission", olButtonsVisibilityDcs], - }).map((entry, idx) => { - return ( -
- {entry[1][0] as string} - { - selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]]; - setSelectionFilter(deepCopyTable(selectionFilter)); - }} - toggled={selectionFilter["control"][entry[0]]} - /> + selectedUnits.forEach((unit) => { + if (!(unit.getName() in unitOccurences[unit.getCoalition()])) + unitOccurences[unit.getCoalition()][unit.getName()] = { occurences: 1, label: unit.getBlueprint()?.label }; + else unitOccurences[unit.getCoalition()][unit.getName()].occurences++; + }); + + const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? []; + + const filteredUnits = Object.values(getApp()?.getUnitsManager()?.getUnits() ?? {}).filter( + (unit) => + unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0 || + (unit.getBlueprint()?.label ?? "").toLowerCase()?.indexOf(filterString.toLowerCase()) >= 0 + ); + + const everyUnitIsGround = selectedCategories.every((category) => { + return category === "GroundUnit"; + }); + const everyUnitIsNavy = selectedCategories.every((category) => { + return category === "NavyUnit"; + }); + const everyUnitIsHelicopter = selectedCategories.every((category) => { + return category === "Helicopter"; + }); + + /* Speed/altitude increments */ + const minAltitude = 0; + const minSpeed = 0; + + let maxAltitude = maxAltitudeValues.aircraft; + let maxSpeed = maxSpeedValues.aircraft; + let speedStep = speedIncrements.aircraft; + let altitudeStep = altitudeIncrements.aircraft; + + if (everyUnitIsHelicopter) { + maxAltitude = maxAltitudeValues.helicopter; + maxSpeed = maxSpeedValues.helicopter; + speedStep = speedIncrements.helicopter; + altitudeStep = altitudeIncrements.helicopter; + } else if (everyUnitIsGround) { + maxSpeed = maxSpeedValues.groundunit; + speedStep = speedIncrements.groundunit; + } else if (everyUnitIsNavy) { + maxSpeed = maxSpeedValues.navyunit; + speedStep = speedIncrements.navyunit; + } + + /* Expand the custom string json if possible */ + const customString = selectedUnits.length > 0 ? selectedUnits[0].getCustomString() : ""; + const customInteger = selectedUnits.length > 0 ? selectedUnits[0].getCustomInteger() : 0; + + // Check if customString is a valid JSON + let customStringJson: { [key: string]: any } | null = null; + try { + customStringJson = JSON.parse(customString); + } catch (e) { + console.error("Invalid JSON string:", customString); + } + + // Used to show custom strings as json, recusively returns divs for arrays + function recursivelyPrintArray(obj: any, depth = 0) { + if (Array.isArray(obj)) { + return ( +
+
[
+ {obj.map((item: any, index: number) => ( +
+ {recursivelyPrintArray(item, depth + 1)}{index < obj.length - 1 ? , : null}
- ); - })} -
- -
- Types and coalitions -
- - )} - - - {selectionID === null && ( - - - - - - - )} - {selectionID === null && - Object.entries({ - aircraft: [olButtonsVisibilityAircraft, "Aircrafts"], - helicopter: [olButtonsVisibilityHelicopter, "Helicopters"], - "groundunit-sam": [olButtonsVisibilityGroundunitSam, "SAMs"], - groundunit: [olButtonsVisibilityGroundunit, "Ground units"], - navyunit: [olButtonsVisibilityNavyunit, "Navy units"], - }).map((entry, idx) => { - return ( - - - {["blue", "neutral", "red"].map((coalition) => { - return ( - - ); - })} - - ); - })} - {selectionID === null && ( - - - - - - - )} - -
BLUENEUTRALRED
- {" "} -
- {entry[1][1] as string} -
-
- { - selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]]; - setSelectionFilter(deepCopyTable(selectionFilter)); - }} - /> -
- value)} - onChange={() => { - const newValue = !Object.values(selectionFilter["blue"]).some((value) => value); - Object.keys(selectionFilter["blue"]).forEach((key) => { - selectionFilter["blue"][key] = newValue; - }); - setSelectionFilter(deepCopyTable(selectionFilter)); - }} - /> - - value)} - onChange={() => { - const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value); - Object.keys(selectionFilter["neutral"]).forEach((key) => { - selectionFilter["neutral"][key] = newValue; - }); - setSelectionFilter(deepCopyTable(selectionFilter)); - }} - /> - - value)} - onChange={() => { - const newValue = !Object.values(selectionFilter["red"]).some((value) => value); - Object.keys(selectionFilter["red"]).forEach((key) => { - selectionFilter["red"][key] = newValue; - }); - setSelectionFilter(deepCopyTable(selectionFilter)); - }} - /> -
-
-
- { - setFilterString(value); - selectionID && setSelectionID(null); - }} - text={selectionID ? (getApp().getUnitsManager().getUnitByID(selectionID)?.getUnitName() ?? "") : filterString} - /> + ))} +
]
- -
- {filterString !== "" && - filteredUnits.length > 0 && - filteredUnits.map((unit) => { - return ( - { - setSelectionID(unit.ID); - }} - > -
{ - unit.setHighlighted(true); - }} - onMouseLeave={() => { - unit.setHighlighted(false); - }} - > - {unit.getUnitName()} ({unit.getBlueprint()?.label ?? ""}) -
-
- ); - })} - {filteredUnits.length == 0 && No results} -
-
-
-
- -
- )} - {/* ============== Selection tool END ============== */} - - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - <> - {/* ============== Unit control menu START ============== */} - {selectedUnits.length > 0 && ( - <> - {Object.keys(unitOccurences["blue"]).length + Object.keys(unitOccurences["neutral"]).length + Object.keys(unitOccurences["red"]).length > 1 && ( -
- {" "} - {" "} -
-
Click: keep as only selection
-
- - {" "} - ctrl - {" "} - + click: deselect{" "} -
-
- - shift - {" "} - + click: keep only units of coalition -
-
{" "} -
- )} - {/* ============== Units list START ============== */} -
-
- { - <> - {["blue", "red", "neutral"].map((coalition) => { - return Object.keys(unitOccurences[coalition]).map((name, idx) => { - return ( -
{ - if (ev.ctrlKey) { - getApp() - .getUnitsManager() - .getSelectedUnits() - .forEach((unit) => { - if (unit.getName() === name && unit.getCoalition() === coalition) { - unit.setSelected(false); - } - }); - } else if (ev.shiftKey) { - getApp() - .getUnitsManager() - .getSelectedUnits() - .forEach((unit) => { - if (unit.getCoalition() !== coalition) { - unit.setSelected(false); - } - }); - } else { - getApp() - .getUnitsManager() - .getSelectedUnits() - .forEach((unit) => { - if (unit.getName() !== name || unit.getCoalition() !== coalition) { - unit.setSelected(false); - } - }); - } - }} - > - - {unitOccurences[coalition][name].label} - - - x{unitOccurences[coalition][name].occurences} - -
- ); - }); - })} - - } -
-
- {/* ============== Units list END ============== */} - {/* ============== Unit basic options START ============== */} - <> - {!showRadioSettings && !showAdvancedSettings && ( -
- {/* ============== Altitude selector START ============== */} - {selectedCategories.every((category) => { - return ["Aircraft", "Helicopter"].includes(category); - }) && ( -
-
-
- - Altitude - - - {selectedUnitsData.desiredAltitude !== undefined - ? Intl.NumberFormat("en-US").format(selectedUnitsData.desiredAltitude) + " FT" - : "Different values"} - -
- { - getApp() - .getUnitsManager() - .setAltitudeType(selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL", null, () => - setForcedUnitsData({ - ...forcedUnitsData, - desiredAltitudeType: selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL", - }) - ); - }} - tooltip={() => ( - - )} - tooltipRelativeToParent={true} - /> -
- { - let value = Number(ev.target.value); - getApp() - .getUnitsManager() - .setAltitude(ftToM(value), null, () => - setForcedUnitsData({ - ...forcedUnitsData, - desiredAltitude: value, - }) - ); - }} - value={selectedUnitsData.desiredAltitude} - min={minAltitude} - max={maxAltitude} - step={altitudeStep} - /> -
- )} - {/* ============== Altitude selector END ============== */} - {/* ============== Airspeed selector START ============== */} -
+ return ( + 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`} + onClose={props.onClose} + autohide={true} + wiki={() => { + return (
-
- - Speed - - - {selectedUnitsData.desiredSpeed !== undefined ? selectedUnitsData.desiredSpeed + " KTS" : "Different values"} - -
- {!(everyUnitIsGround || everyUnitIsNavy) && ( - { - getApp() - .getUnitsManager() - .setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS", null, () => - setForcedUnitsData({ - ...forcedUnitsData, - desiredSpeedType: selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS", - }) - ); - }} - tooltip={() => ( - - )} - tooltipRelativeToParent={true} - /> - )} -
- { - let value = Number(ev.target.value); - getApp() - .getUnitsManager() - .setSpeed(knotsToMs(value), null, () => - setForcedUnitsData({ - ...forcedUnitsData, - desiredSpeed: value, - }) - ); - }} - value={selectedUnitsData.desiredSpeed} - min={minSpeed} - max={maxSpeed} - step={speedStep} - /> -
- {/* ============== Airspeed selector END ============== */} - {/* ============== Rules of Engagement START ============== */} - {!(selectedUnits.length === 1 && selectedUnits[0].isTanker()) && !(selectedUnits.length === 1 && selectedUnits[0].isAWACS()) && ( -
- - Rules of engagement - - ( - -
Sets the rule of engagement of the unit, in order:
-
-
- {" "} - {" "} - Hold fire: The unit will not shoot in any circumstance -
-
- {" "} - {" "} - Return fire: The unit will not fire unless fired upon -
-
- {" "} - {" "} -
- {" "} - Fire on target: The unit will not fire unless fired upon{" "} -

- or -

{" "} - ordered to do so{" "} -
-
-
- {" "} - {" "} - Free: The unit will fire at any detected enemy in range -
-
-
-
- -
-
- Currently, DCS blue and red ground units do not respect{" "} - {" "} - and{" "} - {" "} - rules of engagement, so be careful, they may start shooting when you don't want them to. Use neutral units for finer - control. -
-
-
- } - /> - )} - tooltipRelativeToParent={true} - > - {[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => { - return ( - { - getApp() - .getUnitsManager() - .setROE(ROEs[convertROE(idx)], null, () => - setForcedUnitsData({ - ...forcedUnitsData, - ROE: ROEs[convertROE(idx)], - }) - ); - }} - active={selectedUnitsData.ROE === ROEs[convertROE(idx)]} - icon={icon} - /> - ); - })} - -
- )} - {/* ============== Rules of Engagement END ============== */} - - {/* ============== Alarm state selector START ============== */} - { -
- - Alarm State - - ( - -
Sets the alarm state of the unit, in order:
-
-
- {" "} - Green: The unit will not engage - with its sensors in any circumstances. The unit will be able to move. -
-
- {" "} - {" "} -
Auto: The unit will use its sensors to engage based on its ROE.
-
- -
- {" "} - Red: The unit will be actively - searching for target with its sensors. For some units, this will deploy the radar and make the unit not able to move. -
-
-
- } - /> - )} - tooltipRelativeToParent={true} - > - {[olButtonsAlarmstateGreen, olButtonsAlarmstateAuto, olButtonsAlarmstateRed].map((icon, idx) => { - return ( - { - getApp() - .getUnitsManager() - .setAlarmState([1, 0, 2][idx], null, () => - setForcedUnitsData({ - ...forcedUnitsData, - alarmState: [AlarmState.GREEN, AlarmState.AUTO, AlarmState.RED][idx], - }) - ); - }} - active={selectedUnitsData.alarmState === [AlarmState.GREEN, AlarmState.AUTO, AlarmState.RED][idx]} - icon={icon} - /> - ); - })} - -
- } - {/* ============== Alarm state selector END ============== */} - - {selectedCategories.every((category) => { - return ["Aircraft", "Helicopter"].includes(category); - }) && ( - <> - {/* ============== Threat Reaction START ============== */} -
- - Threat reaction - - ( - -
Sets the reaction to threat of the unit, in order:
-
-
- {" "} - {" "} - No reaction: The unit will not react in any circumstance -
-
- {" "} - {" "} - Passive: The unit will use counter-measures, but will not alter its course -
-
- {" "} - {" "} - Manouevre: The unit will try to evade the threat using manoeuvres, but no counter-measures -
-
- {" "} - {" "} - Full evasion: the unit will try to evade the threat both manoeuvering and using counter-measures -
-
-
- } - /> - )} - tooltipRelativeToParent={true} - > - {[olButtonsThreatNone, olButtonsThreatPassive, olButtonsThreatManoeuvre, olButtonsThreatEvade].map((icon, idx) => { - return ( - { - getApp() - .getUnitsManager() - .setReactionToThreat(reactionsToThreat[idx], null, () => - setForcedUnitsData({ - ...forcedUnitsData, - reactionToThreat: reactionsToThreat[idx], - }) - ); - }} - active={selectedUnitsData.reactionToThreat === reactionsToThreat[idx]} - icon={icon} - /> - ); - })} - -
- {/* ============== Threat Reaction END ============== */} - {/* ============== Radar and ECM START ============== */} -
- - Radar and ECM - - ( - -
Sets the units radar and Electronic Counter Measures (jamming) use policy, in order:
-
-
- {" "} - {" "} - Radio silence: No radar or ECM will be used -
-
- {" "} - {" "} - Defensive: The unit will turn radar and ECM on only when threatened -
-
- {" "} - {" "} - Attack: The unit will use radar and ECM when engaging other units -
-
- {" "} - {" "} - Free: the unit will use the radar and ECM all the time -
-
-
- } - /> - )} - tooltipRelativeToParent={true} - tooltipPosition="above" - > - {[olButtonsEmissionsSilent, olButtonsEmissionsDefend, olButtonsEmissionsAttack, olButtonsEmissionsFree].map((icon, idx) => { - return ( - { - getApp() - .getUnitsManager() - .setEmissionsCountermeasures(emissionsCountermeasures[idx], null, () => - setForcedUnitsData({ - ...forcedUnitsData, - emissionsCountermeasures: emissionsCountermeasures[idx], - }) - ); - }} - active={selectedUnitsData.emissionsCountermeasures === emissionsCountermeasures[idx]} - icon={icon} - /> - ); - })} - - - {/* ============== Radar and ECM END ============== */} - - )} - {/* ============== Tanker and AWACS available button START ============== */} - {getApp() - ?.getUnitsManager() - ?.getSelectedUnitsVariable((unit) => { - return unit.isTanker(); - }) && ( -
- - Make tanker available - - { - if ( - selectedUnitsData.isActiveAWACS !== undefined && - selectedUnitsData.TACAN !== undefined && - selectedUnitsData.radio !== undefined && - selectedUnitsData.generalSettings !== undefined - ) - getApp() - .getUnitsManager() - .setAdvancedOptions( - !selectedUnitsData.isActiveTanker, - selectedUnitsData.isActiveAWACS, - selectedUnitsData.TACAN, - selectedUnitsData.radio, - selectedUnitsData.generalSettings, - null, - () => - setForcedUnitsData({ - ...forcedUnitsData, - isActiveTanker: !selectedUnitsData.isActiveTanker, - }) - ); - }} - tooltip={() => ( - - )} - tooltipRelativeToParent={true} - /> -
- )} - {getApp() - ?.getUnitsManager() - ?.getSelectedUnitsVariable((unit) => { - return unit.isAWACS(); - }) && ( -
- - Make AWACS available - - { - if ( - selectedUnitsData.isActiveTanker !== undefined && - selectedUnitsData.TACAN !== undefined && - selectedUnitsData.radio !== undefined && - selectedUnitsData.generalSettings !== undefined - ) - getApp() - .getUnitsManager() - .setAdvancedOptions( - selectedUnitsData.isActiveTanker, - !selectedUnitsData.isActiveAWACS, - selectedUnitsData.TACAN, - selectedUnitsData.radio, - selectedUnitsData.generalSettings, - null, - () => - setForcedUnitsData({ - ...forcedUnitsData, - isActiveAWACS: !selectedUnitsData.isActiveAWACS, - }) - ); - }} - tooltip={() => ( - - )} - tooltipRelativeToParent={true} - /> -
- )} - {/* ============== Tanker and AWACS available button END ============== */} - {/* ============== Radio settings buttons START ============== */} - {selectedUnits.length === 1 && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && ( -
- -
- )} - {/* ============== Radio settings buttons END ============== */} - {/* ============== Advanced settings buttons START ============== */} - {selectedUnits.length === 1 && - !selectedUnits[0].isTanker() && - !selectedUnits[0].isAWACS() && - ["Aircraft", "Helicopter"].includes(selectedUnits[0].getCategory()) && ( -
- -
- )} - {/* ============== Advanced settings buttons END ============== */} - - {selectedCategories.every((category) => { - return ["GroundUnit", "NavyUnit"].includes(category); - }) && ( - <> -
-
-
- - Scenic modes - - setShowScenicModes(!showScenicModes)} - /> -
- {showScenicModes && ( -
-
-
- -
-
- Currently, DCS blue and red ground units do not respect their rules of engagement, so be careful, they may start shooting when - you don't want them to. Use neutral units for finer control, then use the "Operate as" toggle to switch their "side". -
-
-
- )} +

Unit selection tool

+
+ The unit control menu serves two purposes. If no unit is currently selected, it allows you to select units based on their category, + coalition, and control mode. You can also select units based on their specific type by using the search input.
- {showScenicModes && ( - <> - {/* ============== Scenic AAA toggle START ============== */} -
- - Scenic AAA mode - - { - if (selectedUnitsData.scenicAAA) { - getApp() - .getUnitsManager() - .stop(null, () => - setForcedUnitsData({ - ...forcedUnitsData, - scenicAAA: false, - }) - ); - } else { - getApp() - .getUnitsManager() - .scenicAAA(null, () => - setForcedUnitsData({ - ...forcedUnitsData, - scenicAAA: true, - }) - ); - } - }} - tooltip={() => ( - - )} - tooltipRelativeToParent={true} - /> +

Unit control tool

+
+ If units are selected, the menu will display the selected units and allow you to control their altitude, speed, rules of engagement, + and other settings. +
+
+ The available controls depend on what type of unit is selected. Only controls applicable to every selected unit will be displayed, + so make sure to refine your selection.{" "} +
+
+ {" "} + You will be able to inspect the current values of the controls, e.g. the desired altitude, rules of engagement and so on. However, + if multiple units are selected, you will only see the values of controls that are set to be the same for each selected unit. +
+
+ {" "} + For example, if two airplanes are selected and they both have been instructed to fly at 1000ft, you will see the altitude slider set + at that value. But if one airplane is set to fly at 1000ft and the other at 2000ft, you will see the slider display 'Different + values'. +
+
If at that point you move the slider, you will instruct both airplanes to fly at the same altitude.
+
+ {" "} + If a single unit is selected, you will also be able to see additional info on the unit, like its fuel level, position and altitude, + tasking, and available ammunition.{" "} +
+
+ ); + }} + > + <> + {/* ============== Selection tool START ============== */} + {selectedUnits.length == 0 && ( +
+
Selection tool
+
+
+
- {/* ============== Scenic AAA toggle END ============== */} - {/* ============== Miss on purpose toggle START ============== */} -
- - Miss on purpose mode - - { - if (selectedUnitsData.missOnPurpose) { - getApp() - .getUnitsManager() - .stop(null, () => - setForcedUnitsData({ - ...forcedUnitsData, - missOnPurpose: false, - }) - ); - } else { - getApp() - .getUnitsManager() - .missOnPurpose(null, () => - setForcedUnitsData({ - ...forcedUnitsData, - missOnPurpose: true, - }) - ); - } - }} - tooltip={() => ( - - )} - tooltipRelativeToParent={true} - /> +
+ The selection tools allows you to select units depending on their category, coalition, and control mode. You can also select + units depending on their specific type by using the search input.
- {/* ============== Miss on purpose toggle END ============== */} -
- {/* ============== Shots scatter START ============== */} -
- - Shots scatter - - - {[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => { - return ( - { - getApp() - .getUnitsManager() - .setShotsScatter(idx + 1, null, () => - setForcedUnitsData({ - ...forcedUnitsData, - shotsScatter: idx + 1, - }) +
+
+ {selectionID === null && ( + <> +
+ Control mode +
+ +
+ {Object.entries({ + human: ["Human", olButtonsVisibilityHuman], + olympus: ["Olympus controlled", olButtonsVisibilityOlympus], + dcs: ["From DCS mission", olButtonsVisibilityDcs], + }).map((entry, idx) => { + return ( +
+ + {entry[1][0] as string} + + { + selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]]; + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + toggled={selectionFilter["control"][entry[0]]} + /> +
); + })} +
+ +
+ Types and coalitions +
+ + )} + + + {selectionID === null && ( + + + + + + + )} + {selectionID === null && + Object.entries({ + aircraft: [olButtonsVisibilityAircraft, "Aircrafts"], + helicopter: [olButtonsVisibilityHelicopter, "Helicopters"], + "groundunit-sam": [olButtonsVisibilityGroundunitSam, "SAMs"], + groundunit: [olButtonsVisibilityGroundunit, "Ground units"], + navyunit: [olButtonsVisibilityNavyunit, "Navy units"], + }).map((entry, idx) => { + return ( + + + {["blue", "neutral", "red"].map((coalition) => { + return ( + + ); + })} + + ); + })} + {selectionID === null && ( + + + + + + + )} + +
+ BLUE + + NEUTRAL + + RED +
+ {" "} +
+ {entry[1][1] as string} +
+
+ { + selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]]; + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> +
+ value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["blue"]).some((value) => value); + Object.keys(selectionFilter["blue"]).forEach((key) => { + selectionFilter["blue"][key] = newValue; + }); + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> + + value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value); + Object.keys(selectionFilter["neutral"]).forEach((key) => { + selectionFilter["neutral"][key] = newValue; + }); + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> + + value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["red"]).some((value) => value); + Object.keys(selectionFilter["red"]).forEach((key) => { + selectionFilter["red"][key] = newValue; + }); + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> +
+
+
+ { + setFilterString(value); + selectionID && setSelectionID(null); }} - active={selectedUnitsData.shotsScatter === idx + 1} - icon={icon} - /> - ); - })} - -
- {/* ============== Shots scatter END ============== */} - {/* ============== Shots intensity START ============== */} - {/*
+ text={selectionID ? (getApp().getUnitsManager().getUnitByID(selectionID)?.getUnitName() ?? "") : filterString} + /> +
+ +
+ {filterString !== "" && + filteredUnits.length > 0 && + filteredUnits.map((unit) => { + return ( + { + setSelectionID(unit.ID); + }} + > +
{ + unit.setHighlighted(true); + }} + onMouseLeave={() => { + unit.setHighlighted(false); + }} + > + {unit.getUnitName()} ({unit.getBlueprint()?.label ?? ""}) +
+
+ ); + })} + {filteredUnits.length == 0 && No results} +
+
+
+
+ +
+ )} + {/* ============== Selection tool END ============== */} + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + <> + {/* ============== Unit control menu START ============== */} + {selectedUnits.length > 0 && ( + <> + {Object.keys(unitOccurences["blue"]).length + + Object.keys(unitOccurences["neutral"]).length + + Object.keys(unitOccurences["red"]).length > + 1 && ( +
+ {" "} + {" "} +
+
Click: keep as only selection
+
+ + {" "} + ctrl + {" "} + + click: deselect{" "} +
+
+ + shift + {" "} + + click: keep only units of coalition +
+
{" "} +
+ )} + {/* ============== Units list START ============== */} +
+
+ { + <> + {["blue", "red", "neutral"].map((coalition) => { + return Object.keys(unitOccurences[coalition]).map((name, idx) => { + return ( +
{ + if (ev.ctrlKey) { + getApp() + .getUnitsManager() + .getSelectedUnits() + .forEach((unit) => { + if (unit.getName() === name && unit.getCoalition() === coalition) { + unit.setSelected(false); + } + }); + } else if (ev.shiftKey) { + getApp() + .getUnitsManager() + .getSelectedUnits() + .forEach((unit) => { + if (unit.getCoalition() !== coalition) { + unit.setSelected(false); + } + }); + } else { + getApp() + .getUnitsManager() + .getSelectedUnits() + .forEach((unit) => { + if (unit.getName() !== name || unit.getCoalition() !== coalition) { + unit.setSelected(false); + } + }); + } + }} + > + + {unitOccurences[coalition][name].label} + + + x{unitOccurences[coalition][name].occurences} + +
+ ); + }); + })} + + } +
+
+ {/* ============== Units list END ============== */} + {/* ============== Unit basic options START ============== */} + <> + {!showRadioSettings && !showAdvancedSettings && ( +
+ {/* ============== Altitude selector START ============== */} + {selectedCategories.every((category) => { + return ["Aircraft", "Helicopter"].includes(category); + }) && ( +
+
+
+ + Altitude + + + {selectedUnitsData.desiredAltitude !== undefined + ? Intl.NumberFormat("en-US").format(selectedUnitsData.desiredAltitude) + " FT" + : "Different values"} + +
+ { + getApp() + .getUnitsManager() + .setAltitudeType(selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL", null, () => + setForcedUnitsData({ + ...forcedUnitsData, + desiredAltitudeType: selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL", + }) + ); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + /> +
+ { + let value = Number(ev.target.value); + getApp() + .getUnitsManager() + .setAltitude(ftToM(value), null, () => + setForcedUnitsData({ + ...forcedUnitsData, + desiredAltitude: value, + }) + ); + }} + value={selectedUnitsData.desiredAltitude} + min={minAltitude} + max={maxAltitude} + step={altitudeStep} + /> +
+ )} + {/* ============== Altitude selector END ============== */} + {/* ============== Airspeed selector START ============== */} +
+
+
+ + Speed + + + {selectedUnitsData.desiredSpeed !== undefined + ? selectedUnitsData.desiredSpeed + " KTS" + : "Different values"} + +
+ {!(everyUnitIsGround || everyUnitIsNavy) && ( + { + getApp() + .getUnitsManager() + .setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS", null, () => + setForcedUnitsData({ + ...forcedUnitsData, + desiredSpeedType: selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS", + }) + ); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + /> + )} +
+ { + let value = Number(ev.target.value); + getApp() + .getUnitsManager() + .setSpeed(knotsToMs(value), null, () => + setForcedUnitsData({ + ...forcedUnitsData, + desiredSpeed: value, + }) + ); + }} + value={selectedUnitsData.desiredSpeed} + min={minSpeed} + max={maxSpeed} + step={speedStep} + /> +
+ {/* ============== Airspeed selector END ============== */} + {/* ============== Rules of Engagement START ============== */} + {!(selectedUnits.length === 1 && selectedUnits[0].isTanker()) && + !(selectedUnits.length === 1 && selectedUnits[0].isAWACS()) && ( +
+ + Rules of engagement + + ( + +
Sets the rule of engagement of the unit, in order:
+
+
+ {" "} + {" "} + Hold fire: The unit will not shoot in any circumstance +
+
+ {" "} + {" "} + Return fire: The unit will not fire unless fired upon +
+
+ {" "} + {" "} +
+ {" "} + Fire on target: The unit will not fire unless fired upon{" "} +

+ or +

{" "} + ordered to do so{" "} +
+
+
+ {" "} + {" "} + Free: The unit will fire at any detected enemy in range +
+
+
+
+ +
+
+ Currently, DCS blue and red ground units do not respect{" "} + {" "} + and{" "} + {" "} + rules of engagement, so be careful, they may start shooting when you don't want them + to. Use neutral units for finer control. +
+
+
+ } + /> + )} + tooltipRelativeToParent={true} + > + {[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => { + return ( + { + getApp() + .getUnitsManager() + .setROE(ROEs[convertROE(idx)], null, () => + setForcedUnitsData({ + ...forcedUnitsData, + ROE: ROEs[convertROE(idx)], + }) + ); + }} + active={selectedUnitsData.ROE === ROEs[convertROE(idx)]} + icon={icon} + /> + ); + })} + +
+ )} + {/* ============== Rules of Engagement END ============== */} + + {/* ============== Alarm state selector START ============== */} + { +
+ + Alarm State + + ( + +
Sets the alarm state of the unit, in order:
+
+
+ {" "} + {" "} + Green: The unit will not engage with its sensors in any circumstances. The unit will be + able to move. +
+
+ {" "} + {" "} +
Auto: The unit will use its sensors to engage based on its ROE.
+
+ +
+ {" "} + {" "} + Red: The unit will be actively searching for target with its sensors. For some units, + this will deploy the radar and make the unit not able to move. +
+
+
+ } + /> + )} + tooltipRelativeToParent={true} + > + {[olButtonsAlarmstateGreen, olButtonsAlarmstateAuto, olButtonsAlarmstateRed].map((icon, idx) => { + return ( + { + getApp() + .getUnitsManager() + .setAlarmState([1, 0, 2][idx], null, () => + setForcedUnitsData({ + ...forcedUnitsData, + alarmState: [AlarmState.GREEN, AlarmState.AUTO, AlarmState.RED][idx], + }) + ); + }} + active={selectedUnitsData.alarmState === [AlarmState.GREEN, AlarmState.AUTO, AlarmState.RED][idx]} + icon={icon} + /> + ); + })} + +
+ } + {/* ============== Alarm state selector END ============== */} + + {selectedCategories.every((category) => { + return ["Aircraft", "Helicopter"].includes(category); + }) && ( + <> + {/* ============== Threat Reaction START ============== */} +
+ + Threat reaction + + ( + +
Sets the reaction to threat of the unit, in order:
+
+
+ {" "} + {" "} + No reaction: The unit will not react in any circumstance +
+
+ {" "} + {" "} + Passive: The unit will use counter-measures, but will not alter its course +
+
+ {" "} + {" "} + Manouevre: The unit will try to evade the threat using manoeuvres, but no + counter-measures +
+
+ {" "} + {" "} + Full evasion: the unit will try to evade the threat both manoeuvering and using + counter-measures +
+
+
+ } + /> + )} + tooltipRelativeToParent={true} + > + {[olButtonsThreatNone, olButtonsThreatPassive, olButtonsThreatManoeuvre, olButtonsThreatEvade].map( + (icon, idx) => { + return ( + { + getApp() + .getUnitsManager() + .setReactionToThreat(reactionsToThreat[idx], null, () => + setForcedUnitsData({ + ...forcedUnitsData, + reactionToThreat: reactionsToThreat[idx], + }) + ); + }} + active={selectedUnitsData.reactionToThreat === reactionsToThreat[idx]} + icon={icon} + /> + ); + } + )} + +
+ {/* ============== Threat Reaction END ============== */} + {/* ============== Radar and ECM START ============== */} +
+ + Radar and ECM + + ( + +
+ Sets the units radar and Electronic Counter Measures (jamming) use policy, in order: +
+
+
+ {" "} + {" "} + Radio silence: No radar or ECM will be used +
+
+ {" "} + {" "} + Defensive: The unit will turn radar and ECM on only when threatened +
+
+ {" "} + {" "} + Attack: The unit will use radar and ECM when engaging other units +
+
+ {" "} + {" "} + Free: the unit will use the radar and ECM all the time +
+
+
+ } + /> + )} + tooltipRelativeToParent={true} + tooltipPosition="above" + > + {[olButtonsEmissionsSilent, olButtonsEmissionsDefend, olButtonsEmissionsAttack, olButtonsEmissionsFree].map( + (icon, idx) => { + return ( + { + getApp() + .getUnitsManager() + .setEmissionsCountermeasures(emissionsCountermeasures[idx], null, () => + setForcedUnitsData({ + ...forcedUnitsData, + emissionsCountermeasures: emissionsCountermeasures[idx], + }) + ); + }} + active={selectedUnitsData.emissionsCountermeasures === emissionsCountermeasures[idx]} + icon={icon} + /> + ); + } + )} + +
+ {/* ============== Radar and ECM END ============== */} + + )} + {/* ============== Tanker and AWACS available button START ============== */} + {getApp() + ?.getUnitsManager() + ?.getSelectedUnitsVariable((unit) => { + return unit.isTanker(); + }) && ( +
+ + Make tanker available + + { + if ( + selectedUnitsData.isActiveAWACS !== undefined && + selectedUnitsData.TACAN !== undefined && + selectedUnitsData.radio !== undefined && + selectedUnitsData.generalSettings !== undefined + ) + getApp() + .getUnitsManager() + .setAdvancedOptions( + !selectedUnitsData.isActiveTanker, + selectedUnitsData.isActiveAWACS, + selectedUnitsData.TACAN, + selectedUnitsData.radio, + selectedUnitsData.generalSettings, + null, + () => + setForcedUnitsData({ + ...forcedUnitsData, + isActiveTanker: !selectedUnitsData.isActiveTanker, + }) + ); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + /> +
+ )} + {getApp() + ?.getUnitsManager() + ?.getSelectedUnitsVariable((unit) => { + return unit.isAWACS(); + }) && ( +
+ + Make AWACS available + + { + if ( + selectedUnitsData.isActiveTanker !== undefined && + selectedUnitsData.TACAN !== undefined && + selectedUnitsData.radio !== undefined && + selectedUnitsData.generalSettings !== undefined + ) + getApp() + .getUnitsManager() + .setAdvancedOptions( + selectedUnitsData.isActiveTanker, + !selectedUnitsData.isActiveAWACS, + selectedUnitsData.TACAN, + selectedUnitsData.radio, + selectedUnitsData.generalSettings, + null, + () => + setForcedUnitsData({ + ...forcedUnitsData, + isActiveAWACS: !selectedUnitsData.isActiveAWACS, + }) + ); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + /> +
+ )} + {/* ============== Tanker and AWACS available button END ============== */} + {/* ============== Radio settings buttons START ============== */} + {selectedUnits.length === 1 && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && ( +
+ +
+ )} + {/* ============== Radio settings buttons END ============== */} + {/* ============== Advanced settings buttons START ============== */} + {selectedUnits.length === 1 && + !selectedUnits[0].isTanker() && + !selectedUnits[0].isAWACS() && + ["Aircraft", "Helicopter"].includes(selectedUnits[0].getCategory()) && ( +
+ +
+ )} + {/* ============== Advanced settings buttons END ============== */} + + {selectedCategories.every((category) => { + return ["GroundUnit", "NavyUnit"].includes(category); + }) && ( + <> +
+
+
+ + Scenic modes + + setShowScenicModes(!showScenicModes)} + /> +
+ {showScenicModes && ( +
+
+
+ +
+
+ Currently, DCS blue and red ground units do not respect their rules of engagement, so be + careful, they may start shooting when you don't want them to. Use neutral units for finer + control, then use the "Operate as" toggle to switch their "side". +
+
+
+ )} +
+ {showScenicModes && ( + <> + {/* ============== Scenic AAA toggle START ============== */} +
+ + Scenic AAA mode + + { + if (selectedUnitsData.scenicAAA) { + getApp() + .getUnitsManager() + .stop(null, () => + setForcedUnitsData({ + ...forcedUnitsData, + scenicAAA: false, + }) + ); + } else { + getApp() + .getUnitsManager() + .scenicAAA(null, () => + setForcedUnitsData({ + ...forcedUnitsData, + scenicAAA: true, + }) + ); + } + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + /> +
+ {/* ============== Scenic AAA toggle END ============== */} + {/* ============== Miss on purpose toggle START ============== */} +
+ + Miss on purpose mode + + { + if (selectedUnitsData.missOnPurpose) { + getApp() + .getUnitsManager() + .stop(null, () => + setForcedUnitsData({ + ...forcedUnitsData, + missOnPurpose: false, + }) + ); + } else { + getApp() + .getUnitsManager() + .missOnPurpose(null, () => + setForcedUnitsData({ + ...forcedUnitsData, + missOnPurpose: true, + }) + ); + } + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + /> +
+ {/* ============== Miss on purpose toggle END ============== */} +
+ {/* ============== Shots scatter START ============== */} +
+ + Shots scatter + + + {[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => { + return ( + { + getApp() + .getUnitsManager() + .setShotsScatter(idx + 1, null, () => + setForcedUnitsData({ + ...forcedUnitsData, + shotsScatter: idx + 1, + }) + ); + }} + active={selectedUnitsData.shotsScatter === idx + 1} + icon={icon} + /> + ); + })} + +
+ {/* ============== Shots scatter END ============== */} + {/* ============== Shots intensity START ============== */} + {/*
void }) {
{/* ============== Shots intensity END ============== */} - {/* setShowEngagementSettings(!showEngagementSettings)} icon={faCog} > */} -
- {/* ============== Operate as toggle START ============== */} - {selectedUnits.every((unit) => unit.getCoalition() === "neutral") && ( -
- - Operate as - - { - getApp() - .getUnitsManager() - .setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue", null, () => - setForcedUnitsData({ - ...forcedUnitsData, - operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue", - }) - ); - }} - tooltip={() => ( - - )} - tooltipRelativeToParent={true} - tooltipPosition="above" - /> -
+
+ {/* ============== Operate as toggle START ============== */} + {selectedUnits.every((unit) => unit.getCoalition() === "neutral") && ( +
+ + Operate as + + { + getApp() + .getUnitsManager() + .setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue", null, () => + setForcedUnitsData({ + ...forcedUnitsData, + operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue", + }) + ); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + tooltipPosition="above" + /> +
+ )} + {/* ============== Operate as toggle END ============== */} + {showEngagementSettings && ( +
+
+
+ Barrel height:{" "} +
+ { + setBarrelHeight(Number(ev.target.value)); + }} + onIncrease={() => { + setBarrelHeight(barrelHeight + 0.1); + }} + onDecrease={() => { + setBarrelHeight(barrelHeight - 0.1); + }} + > +
+ m +
+
+
+
+ Muzzle velocity:{" "} +
+ { + setMuzzleVelocity(Number(ev.target.value)); + }} + onIncrease={() => { + setMuzzleVelocity(muzzleVelocity + 10); + }} + onDecrease={() => { + setMuzzleVelocity(muzzleVelocity - 10); + }} + > +
+ m/s +
+
+
+
+ Aim time:{" "} +
+ { + setAimTime(Number(ev.target.value)); + }} + onIncrease={() => { + setAimTime(aimTime + 0.1); + }} + onDecrease={() => { + setAimTime(aimTime - 0.1); + }} + > +
+ s +
+
+
+
+ Shots to fire:{" "} +
+ { + setShotsToFire(Number(ev.target.value)); + }} + onIncrease={() => { + setShotsToFire(shotsToFire + 1); + }} + onDecrease={() => { + setShotsToFire(shotsToFire - 1); + }} + > +
+
+
+ Shots base interval:{" "} +
+ { + setShotsBaseInterval(Number(ev.target.value)); + }} + onIncrease={() => { + setShotsBaseInterval(shotsBaseInterval + 0.1); + }} + onDecrease={() => { + setShotsBaseInterval(shotsBaseInterval - 0.1); + }} + > +
+ s +
+
+
+
+ Shots base scatter:{" "} +
+ { + setShotsBaseScatter(Number(ev.target.value)); + }} + onIncrease={() => { + setShotsBaseScatter(shotsBaseScatter + 0.1); + }} + onDecrease={() => { + setShotsBaseScatter(shotsBaseScatter - 0.1); + }} + > +
+ deg +
+
+
+
+ Engagement range:{" "} +
+ { + setEngagementRange(Number(ev.target.value)); + }} + onIncrease={() => { + setEngagementRange(engagementRange + 100); + }} + onDecrease={() => { + setEngagementRange(engagementRange - 100); + }} + > +
+ m +
+
+
+
+ Targeting range:{" "} +
+ { + setTargetingRange(Number(ev.target.value)); + }} + onIncrease={() => { + setTargetingRange(targetingRange + 100); + }} + onDecrease={() => { + setTargetingRange(targetingRange - 100); + }} + > +
+ m +
+
+
+
+ Aim method range:{" "} +
+ { + setAimMethodRange(Number(ev.target.value)); + }} + onIncrease={() => { + setAimMethodRange(aimMethodRange + 100); + }} + onDecrease={() => { + setAimMethodRange(aimMethodRange - 100); + }} + > +
+ m +
+
+
+
+ Acquisition range:{" "} +
+ { + setAcquisitionRange(Number(ev.target.value)); + }} + onIncrease={() => { + setAcquisitionRange(acquisitionRange + 100); + }} + onDecrease={() => { + setAcquisitionRange(acquisitionRange - 100); + }} + > +
+ m +
+
+ +
+ )} + + )} +
+ {/* ============== Follow roads toggle START ============== */} + {selectedCategories.every((category) => category === "GroundUnit") && ( +
+ + Follow roads + + { + getApp() + .getUnitsManager() + .setFollowRoads(!selectedUnitsData.followRoads, null, () => + setForcedUnitsData({ + ...forcedUnitsData, + followRoads: !selectedUnitsData.followRoads, + }) + ); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + tooltipPosition="above" + /> +
+ )} + {/* ============== Follow roads toggle END ============== */} + {/* ============== Unit active toggle START ============== */} +
+ + Unit active + + { + getApp() + .getUnitsManager() + .setOnOff(!selectedUnitsData.onOff, null, () => + setForcedUnitsData({ + ...forcedUnitsData, + onOff: !selectedUnitsData.onOff, + }) + ); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + tooltipPosition="above" + /> +
+ {/* ============== Unit active toggle END ============== */} + + )} + {/* ============== Audio sink toggle START ============== */} + {selectedUnits.length === 1 && ( +
+ + Loudspeakers + + {audioManagerRunning ? ( + { + selectedUnits.forEach((unit) => { + if (!selectedUnitsData.isAudioSink) { + getApp()?.getAudioManager().addUnitSink(unit); + setForcedUnitsData({ + ...forcedUnitsData, + isAudioSink: true, + }); + } else { + let sink = getApp() + ?.getAudioManager() + .getSinks() + .find((sink) => { + return sink instanceof UnitSink && sink.getUnit() === unit; + }); + if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink); + + setForcedUnitsData({ + ...forcedUnitsData, + isAudioSink: false, + }); + } + }); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + tooltipPosition="above" + /> + ) : ( +
+ Enable audio with{" "} + + + {" "} + first +
+ )} +
+ )} + {/* ============== Audio sink toggle END ============== */} +
)} - {/* ============== Operate as toggle END ============== */} - {showEngagementSettings && ( -
-
-
Barrel height:
- { - setBarrelHeight(Number(ev.target.value)); - }} - onIncrease={() => { - setBarrelHeight(barrelHeight + 0.1); - }} - onDecrease={() => { - setBarrelHeight(barrelHeight - 0.1); - }} - > -
m
-
-
-
Muzzle velocity:
- { - setMuzzleVelocity(Number(ev.target.value)); - }} - onIncrease={() => { - setMuzzleVelocity(muzzleVelocity + 10); - }} - onDecrease={() => { - setMuzzleVelocity(muzzleVelocity - 10); - }} - > -
m/s
-
-
-
Aim time:
- { - setAimTime(Number(ev.target.value)); - }} - onIncrease={() => { - setAimTime(aimTime + 0.1); - }} - onDecrease={() => { - setAimTime(aimTime - 0.1); - }} - > -
s
-
-
-
Shots to fire:
- { - setShotsToFire(Number(ev.target.value)); - }} - onIncrease={() => { - setShotsToFire(shotsToFire + 1); - }} - onDecrease={() => { - setShotsToFire(shotsToFire - 1); - }} - > -
-
-
Shots base interval:
- { - setShotsBaseInterval(Number(ev.target.value)); - }} - onIncrease={() => { - setShotsBaseInterval(shotsBaseInterval + 0.1); - }} - onDecrease={() => { - setShotsBaseInterval(shotsBaseInterval - 0.1); - }} - > -
s
-
-
-
Shots base scatter:
- { - setShotsBaseScatter(Number(ev.target.value)); - }} - onIncrease={() => { - setShotsBaseScatter(shotsBaseScatter + 0.1); - }} - onDecrease={() => { - setShotsBaseScatter(shotsBaseScatter - 0.1); - }} - > -
deg
-
-
-
Engagement range:
- { - setEngagementRange(Number(ev.target.value)); - }} - onIncrease={() => { - setEngagementRange(engagementRange + 100); - }} - onDecrease={() => { - setEngagementRange(engagementRange - 100); - }} - > -
m
-
-
-
Targeting range:
- { - setTargetingRange(Number(ev.target.value)); - }} - onIncrease={() => { - setTargetingRange(targetingRange + 100); - }} - onDecrease={() => { - setTargetingRange(targetingRange - 100); - }} - > -
m
-
-
-
Aim method range:
- { - setAimMethodRange(Number(ev.target.value)); - }} - onIncrease={() => { - setAimMethodRange(aimMethodRange + 100); - }} - onDecrease={() => { - setAimMethodRange(aimMethodRange - 100); - }} - > -
m
-
-
-
Acquisition range:
- { - setAcquisitionRange(Number(ev.target.value)); - }} - onIncrease={() => { - setAcquisitionRange(acquisitionRange + 100); - }} - onDecrease={() => { - setAcquisitionRange(acquisitionRange - 100); - }} - > -
m
-
- -
+
Radio settings
+
Callsign
+
+ + <> + {selectedUnits[0].isAWACS() && ( + <> + {["Overlord", "Magic", "Wizard", "Focus", "Darkstar"].map((name, idx) => { + return ( + { + if (activeRadioSettings) activeRadioSettings.radio.callsign = idx + 1; + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + > + {name} + + ); + })} + + )} + + <> + {selectedUnits[0].isTanker() && ( + <> + {["Texaco", "Arco", "Shell"].map((name, idx) => { + return ( + { + if (activeRadioSettings) activeRadioSettings.radio.callsign = idx + 1; + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + > + {name} + + ); + })} + + )} + + +
-
+ + { + if (activeRadioSettings) + activeRadioSettings.radio.callsignNumber = Math.max(Math.min(Number(e.target.value), 9), 1); + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + onDecrease={() => { + if (activeRadioSettings) + activeRadioSettings.radio.callsignNumber = Math.max( + Math.min(Number(activeRadioSettings.radio.callsignNumber - 1), 9), + 1 + ); + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + onIncrease={() => { + if (activeRadioSettings) + activeRadioSettings.radio.callsignNumber = Math.max( + Math.min(Number(activeRadioSettings.radio.callsignNumber + 1), 9), + 1 + ); + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + value={activeRadioSettings ? activeRadioSettings.radio.callsignNumber : 1} + > +
+
TACAN
+
+ { + if (activeRadioSettings) activeRadioSettings.TACAN.channel = Math.max(Math.min(Number(e.target.value), 126), 1); + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + onDecrease={() => { + if (activeRadioSettings) + activeRadioSettings.TACAN.channel = Math.max( + Math.min(Number(activeRadioSettings.TACAN.channel - 1), 126), + 1 + ); + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + onIncrease={() => { + if (activeRadioSettings) + activeRadioSettings.TACAN.channel = Math.max( + Math.min(Number(activeRadioSettings.TACAN.channel + 1), 126), + 1 + ); + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + value={activeRadioSettings ? activeRadioSettings.TACAN.channel : 1} + > + + + { + if (activeRadioSettings) activeRadioSettings.TACAN.XY = "X"; + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + > + X + + { + if (activeRadioSettings) activeRadioSettings.TACAN.XY = "Y"; + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + > + Y + + + { + if (activeRadioSettings) { + activeRadioSettings.TACAN.callsign = e.target.value; + if (activeRadioSettings.TACAN.callsign.length > 3) + activeRadioSettings.TACAN.callsign = activeRadioSettings.TACAN.callsign.slice(0, 3); + } + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + /> +
+
+ Enable TACAN{" "} + { + if (activeRadioSettings) activeRadioSettings.TACAN.isOn = !activeRadioSettings.TACAN.isOn; + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + }} + /> +
+ +
Radio frequency
+
+ { + if (activeRadioSettings) { + activeRadioSettings.radio.frequency = value; + setActiveRadioSettings(deepCopyTable(activeRadioSettings)); + } + }} + /> +
+ +
+ + +
+ )} - - )} - - {/* ============== Follow roads toggle START ============== */} - {selectedCategories.every((category) => category === "GroundUnit") && ( -
- - Follow roads - - { - getApp() - .getUnitsManager() - .setFollowRoads(!selectedUnitsData.followRoads, null, () => - setForcedUnitsData({ - ...forcedUnitsData, - followRoads: !selectedUnitsData.followRoads, - }) - ); - }} - tooltip={() => ( - + {/* ============== Radio settings END ============== */} + {/* ============== Advanced settings START ============== */} + {showAdvancedSettings && ( +
+
Radio settings
+
+ Prohibit AA + { + setActiveAdvancedSettings({ + ...(activeAdvancedSettings as GeneralSettings), + prohibitAA: !activeAdvancedSettings?.prohibitAA, + }); + }} + toggled={activeAdvancedSettings?.prohibitAA} + /> +
+
+ Prohibit AG + { + setActiveAdvancedSettings({ + ...(activeAdvancedSettings as GeneralSettings), + prohibitAG: !activeAdvancedSettings?.prohibitAG, + }); + }} + toggled={activeAdvancedSettings?.prohibitAG} + /> +
+
+ Prohibit Jettison + { + setActiveAdvancedSettings({ + ...(activeAdvancedSettings as GeneralSettings), + prohibitJettison: !activeAdvancedSettings?.prohibitJettison, + }); + }} + toggled={activeAdvancedSettings?.prohibitJettison} + /> +
+
+ Prohibit afterburner + { + setActiveAdvancedSettings({ + ...(activeAdvancedSettings as GeneralSettings), + prohibitAfterburner: !activeAdvancedSettings?.prohibitAfterburner, + }); + }} + toggled={activeAdvancedSettings?.prohibitAfterburner} + /> +
+ +
+ + +
+
)} - tooltipRelativeToParent={true} - tooltipPosition="above" - /> -
- )} - {/* ============== Follow roads toggle END ============== */} - {/* ============== Unit active toggle START ============== */} -
- - Unit active - - { - getApp() - .getUnitsManager() - .setOnOff(!selectedUnitsData.onOff, null, () => - setForcedUnitsData({ - ...forcedUnitsData, - onOff: !selectedUnitsData.onOff, - }) - ); - }} - tooltip={() => ( - - )} - tooltipRelativeToParent={true} - tooltipPosition="above" - /> -
- {/* ============== Unit active toggle END ============== */} - - )} - {/* ============== Audio sink toggle START ============== */} - {selectedUnits.length === 1 && ( -
- - Loudspeakers - - {audioManagerRunning ? ( - { - selectedUnits.forEach((unit) => { - if (!selectedUnitsData.isAudioSink) { - getApp()?.getAudioManager().addUnitSink(unit); - setForcedUnitsData({ - ...forcedUnitsData, - isAudioSink: true, - }); - } else { - let sink = getApp() - ?.getAudioManager() - .getSinks() - .find((sink) => { - return sink instanceof UnitSink && sink.getUnit() === unit; - }); - if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink); - - setForcedUnitsData({ - ...forcedUnitsData, - isAudioSink: false, - }); - } - }); - }} - tooltip={() => ( - - )} - tooltipRelativeToParent={true} - tooltipPosition="above" - /> - ) : ( -
- Enable audio with{" "} - - - {" "} - first -
- )} -
- )} - {/* ============== Audio sink toggle END ============== */} - - )} - {/* ============== Radio settings START ============== */} - {showRadioSettings && ( -
-
Radio settings
-
Callsign
-
- - <> - {selectedUnits[0].isAWACS() && ( - <> - {["Overlord", "Magic", "Wizard", "Focus", "Darkstar"].map((name, idx) => { - return ( - { - if (activeRadioSettings) activeRadioSettings.radio.callsign = idx + 1; - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} + {/* ============== Advanced settings END ============== */} + + {/* ============== Unit basic options END ============== */} + <> + {/* ============== Fuel/payload/radio section START ============== */} + {selectedUnits.length === 1 && ( +
- {name} - - ); - })} - - )} - - <> - {selectedUnits[0].isTanker() && ( - <> - {["Texaco", "Arco", "Shell"].map((name, idx) => { - return ( - { - if (activeRadioSettings) activeRadioSettings.radio.callsign = idx + 1; - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - > - {name} - - ); - })} - - )} - - -
-
+
+
+
{selectedUnits[0].getUnitName()}
+
40 && + ` + bg-green-700 + ` + } + ${ + selectedUnits[0].getFuel() > 10 && + selectedUnits[0].getFuel() <= 40 && + ` + bg-yellow-700 + ` + } + ${ + selectedUnits[0].getFuel() <= 10 && + ` + bg-red-700 + ` + } + px-2 py-1 text-sm font-bold + text-white + `} + > + + {selectedUnits[0].getFuel()}% +
+
- { - if (activeRadioSettings) activeRadioSettings.radio.callsignNumber = Math.max(Math.min(Number(e.target.value), 9), 1); - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - onDecrease={() => { - if (activeRadioSettings) - activeRadioSettings.radio.callsignNumber = Math.max(Math.min(Number(activeRadioSettings.radio.callsignNumber - 1), 9), 1); - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - onIncrease={() => { - if (activeRadioSettings) - activeRadioSettings.radio.callsignNumber = Math.max(Math.min(Number(activeRadioSettings.radio.callsignNumber + 1), 9), 1); - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - value={activeRadioSettings ? activeRadioSettings.radio.callsignNumber : 1} - > -
-
TACAN
-
- { - if (activeRadioSettings) activeRadioSettings.TACAN.channel = Math.max(Math.min(Number(e.target.value), 126), 1); - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - onDecrease={() => { - if (activeRadioSettings) activeRadioSettings.TACAN.channel = Math.max(Math.min(Number(activeRadioSettings.TACAN.channel - 1), 126), 1); - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - onIncrease={() => { - if (activeRadioSettings) activeRadioSettings.TACAN.channel = Math.max(Math.min(Number(activeRadioSettings.TACAN.channel + 1), 126), 1); - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - value={activeRadioSettings ? activeRadioSettings.TACAN.channel : 1} - > +
+ {selectedUnits[0].getTask()} +
+
+ ID: {selectedUnits[0].ID} +
+
+ {customStringJson && + Object.keys(customStringJson).map((key) => { + // If the key is an array, put each value on a new line - - { - if (activeRadioSettings) activeRadioSettings.TACAN.XY = "X"; - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - > - X - - { - if (activeRadioSettings) activeRadioSettings.TACAN.XY = "Y"; - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - > - Y - - - { - if (activeRadioSettings) { - activeRadioSettings.TACAN.callsign = e.target.value; - if (activeRadioSettings.TACAN.callsign.length > 3) - activeRadioSettings.TACAN.callsign = activeRadioSettings.TACAN.callsign.slice(0, 3); - } - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - /> -
-
- Enable TACAN{" "} - { - if (activeRadioSettings) activeRadioSettings.TACAN.isOn = !activeRadioSettings.TACAN.isOn; - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - }} - /> -
- -
Radio frequency
-
- { - if (activeRadioSettings) { - activeRadioSettings.radio.frequency = value; - setActiveRadioSettings(deepCopyTable(activeRadioSettings)); - } - }} - /> -
- -
- - -
-
- )} - {/* ============== Radio settings END ============== */} - {/* ============== Advanced settings START ============== */} - {showAdvancedSettings && ( -
-
Radio settings
-
- Prohibit AA - { - setActiveAdvancedSettings({ ...(activeAdvancedSettings as GeneralSettings), prohibitAA: !activeAdvancedSettings?.prohibitAA }); - }} - toggled={activeAdvancedSettings?.prohibitAA} - /> -
-
- Prohibit AG - { - setActiveAdvancedSettings({ ...(activeAdvancedSettings as GeneralSettings), prohibitAG: !activeAdvancedSettings?.prohibitAG }); - }} - toggled={activeAdvancedSettings?.prohibitAG} - /> -
-
- Prohibit Jettison - { - setActiveAdvancedSettings({ - ...(activeAdvancedSettings as GeneralSettings), - prohibitJettison: !activeAdvancedSettings?.prohibitJettison, - }); - }} - toggled={activeAdvancedSettings?.prohibitJettison} - /> -
-
- Prohibit afterburner - { - setActiveAdvancedSettings({ - ...(activeAdvancedSettings as GeneralSettings), - prohibitAfterburner: !activeAdvancedSettings?.prohibitAfterburner, - }); - }} - toggled={activeAdvancedSettings?.prohibitAfterburner} - /> -
- -
- - -
-
- )} - {/* ============== Advanced settings END ============== */} - - {/* ============== Unit basic options END ============== */} - <> - {/* ============== Fuel/payload/radio section START ============== */} - {selectedUnits.length === 1 && ( -
-
-
-
{selectedUnits[0].getUnitName()}
-
40 && `bg-green-700`} - ${ - selectedUnits[0].getFuel() > 10 && - selectedUnits[0].getFuel() <= 40 && - `bg-yellow-700` - } - ${selectedUnits[0].getFuel() <= 10 && `bg-red-700`} - px-2 py-1 text-sm font-bold text-white - `} - > - - {selectedUnits[0].getFuel()}% -
-
- -
{selectedUnits[0].getTask()}
- {/* Useful for debugging but very data hungry + return ( +
+
+ {key} +
+
+ {recursivelyPrintArray(customStringJson[key])} +
+
+ ); + })} +
+ {/* Useful for debugging but very data hungry ([UnitState.SIMULATE_FIRE_FIGHT, UnitState.MISS_ON_PURPOSE, UnitState.SCENIC_AAA] as string[]).includes(selectedUnits[0].getState()) && (
@@ -2317,131 +3111,222 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
)*/} -
- + -
{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft
-
-
+ `} + /> +
+ {Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft +
+
+
-
- {selectedUnits[0].isControlledByOlympus() && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && ( - <> - {/* ============== Radio section START ============== */} -
-
-
- -
-
- {`${selectedUnits[0].isTanker() ? ["Texaco", "Arco", "Shell"][selectedUnits[0].getRadio().callsign - 1] : ["Overlord", "Magic", "Wizard", "Focus", "Darkstar"][selectedUnits[0].getRadio().callsign - 1]}-${selectedUnits[0].getRadio().callsignNumber}`} -
-
-
-
-
-
- -
-
- {`${(selectedUnits[0].getRadio().frequency / 1000000).toFixed(3)} MHz`} -
-
-
+
+ {selectedUnits[0].isControlledByOlympus() && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && ( + <> + {/* ============== Radio section START ============== */} +
+
+
+ +
+
+ {`${selectedUnits[0].isTanker() ? ["Texaco", "Arco", "Shell"][selectedUnits[0].getRadio().callsign - 1] : ["Overlord", "Magic", "Wizard", "Focus", "Darkstar"][selectedUnits[0].getRadio().callsign - 1]}-${selectedUnits[0].getRadio().callsignNumber}`} +
+
+
+
+
+
+ +
+
+ {`${(selectedUnits[0].getRadio().frequency / 1000000).toFixed(3)} MHz`} +
+
+
-
-
-
- -
-
- {selectedUnits[0].getTACAN().isOn - ? `${selectedUnits[0].getTACAN().channel}${selectedUnits[0].getTACAN().XY} ${selectedUnits[0].getTACAN().callsign}` - : "TACAN OFF"} -
-
- {/* ============== Radio section END ============== */} -
- - )} - {/* ============== Payload section START ============== */} - {!selectedUnits[0].isTanker() && - !selectedUnits[0].isAWACS() && - selectedUnits[0].getAmmo().map((ammo, idx) => { - return ( -
-
- {ammo.quantity} -
-
- {ammo.name} -
-
- ); - })} +
+
+
+ +
+
+ {selectedUnits[0].getTACAN().isOn + ? `${selectedUnits[0].getTACAN().channel}${selectedUnits[0].getTACAN().XY} ${selectedUnits[0].getTACAN().callsign}` + : "TACAN OFF"} +
+
+ {/* ============== Radio section END ============== */} +
+ + )} + {/* ============== Payload section START ============== */} + {!selectedUnits[0].isTanker() && + !selectedUnits[0].isAWACS() && + selectedUnits[0].getAmmo().map((ammo, idx) => { + return ( +
+
+ {ammo.quantity} +
+
+ {ammo.name} +
+
+ ); + })} - {/* ============== Payload section END ============== */} -
-
- )} + {/* ============== Payload section END ============== */} +
+ + )} + + {/* ============== Fuel/payload/radio section END ============== */} + + )} + {/* ============== Unit control menu END ============== */} - {/* ============== Fuel/payload/radio section END ============== */} - - )} - {/* ============== Unit control menu END ============== */} - -
- ); + + ); } diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index d8074cad..dc26392d 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -161,6 +161,8 @@ export abstract class Unit extends CustomMarker { #airborne: boolean = false; #cargoWeight: number = 0; #drawingArguments: DrawingArgument[] = []; + #customString: string = ""; + #customInteger: number = 0; /* Other members used to draw the unit, mostly ancillary stuff like targets, ranges and so on */ #blueprint: UnitBlueprint | null = null; @@ -414,6 +416,12 @@ export abstract class Unit extends CustomMarker { getDrawingArguments() { return this.#drawingArguments; } + getCustomString() { + return this.#customString; + } + getCustomInteger() { + return this.#customInteger; + } static getConstructor(type: string) { if (type === "GroundUnit") return GroundUnit; @@ -811,6 +819,12 @@ export abstract class Unit extends CustomMarker { case DataIndexes.drawingArguments: this.#drawingArguments = dataExtractor.extractDrawingArguments(); break; + case DataIndexes.customString: + this.#customString = dataExtractor.extractString(); + break; + case DataIndexes.customInteger: + this.#customInteger = dataExtractor.extractUInt32(); + break; default: break; } @@ -936,6 +950,8 @@ export abstract class Unit extends CustomMarker { airborne: this.#airborne, cargoWeight: this.#cargoWeight, drawingArguments: this.#drawingArguments, + customString: this.#customString, + customInteger: this.#customInteger }; } diff --git a/scripts/python/API/.vscode/launch.json b/scripts/python/API/.vscode/launch.json index 9870d49a..94677d51 100644 --- a/scripts/python/API/.vscode/launch.json +++ b/scripts/python/API/.vscode/launch.json @@ -51,6 +51,14 @@ "program": "example_precise_movement.py", "console": "integratedTerminal", "justMyCode": false, + }, + { + "name": "Infantry boarding", + "type": "debugpy", + "request": "launch", + "program": "infantry_boarding.py", + "console": "integratedTerminal", + "justMyCode": false, } ] } \ No newline at end of file diff --git a/scripts/python/API/data/data_indexes.py b/scripts/python/API/data/data_indexes.py index 54715d97..a7aab7c0 100644 --- a/scripts/python/API/data/data_indexes.py +++ b/scripts/python/API/data/data_indexes.py @@ -69,4 +69,6 @@ class DataIndexes(Enum): AIRBORNE = 65 CARGO_WEIGHT = 66 DRAW_ARGUMENTS = 67 + CUSTOM_STRING = 68 + CUSTOM_INTEGER = 69 END_OF_DATA = 255 \ No newline at end of file diff --git a/scripts/python/API/infantry_boarding.py b/scripts/python/API/infantry_boarding.py new file mode 100644 index 00000000..d8cc5274 --- /dev/null +++ b/scripts/python/API/infantry_boarding.py @@ -0,0 +1,625 @@ +import asyncio +from asyncio import Semaphore +import json +from random import randrange +from api import API, Unit, UnitSpawnTable +from math import pi +import logging + +#Set some globals up +alternate_time = 300 +before_can_re_embark_time = 300 +####Transport types##### +transport_ground = { + "M-113": { + "max_capacity": 4, + "max_embark_range": 50, + "doors": 1, + "door_positions": [(3.35,pi),(0,0)], + "board_positions": [(15,pi),(0,0)], + "door_argument_nos": None, + "door_open_thresholds": None, + "is_rear_loader": True, + "boarding_distance": 5 + } + } + +transport_helicopters = { + "UH-1H":{ + "max_capacity": 8, + "max_embark_range": 100, + "doors": 2, + "door_positions": [(2.5,-pi/2),(0.8,0),(2.5,pi/2),(0.8,0)], #two values here offset and heading offset in radians and second distance offset and heading offset in radians + "board_positions": [(15,-pi/2),(0,0),(15,pi/2),(0,0)], + "door_argument_nos": [43,44], #draw argument numbers for the doors + "door_open_thresholds": [0.8,0.8], #value above which the door is considered open + "is_rear_loader": False, + "boarding_distance": 5 + } + } + +transport_types = set(transport_helicopters.keys()).union(transport_ground.keys()) + +#Infantry transport +embarker_inf_red = {} +embarker_inf_blue = {"Soldier M4 GRG","soldier_wwii_us"} +embarker_types = embarker_inf_blue.union(embarker_inf_red) + +#Time it takes after loading or unloading to swap back to the other + +# Setup a logger for the module +logger = logging.getLogger("infantry_transport") +logger.setLevel(logging.INFO) +handler = logging.StreamHandler() +formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) + +class Transporter(Unit): + def __init__(self, Unit): + self.unit = Unit + + def to_json(self): + return { + "is_transport": self.unit.is_transport, + "max_capacity": self.unit.max_capacity, + "current_capacity": self.unit.current_capacity, + "max_embark_range": self.unit.max_embark_range, + "boarding_distance": self.unit.boarding_distance, + "current_cargo_weight": self.unit.current_cargo_weight, + "unit_array": [unit.ID for unit in self.unit.unit_array], + "en_boarding_queue": [unit.ID for unit in self.unit.en_boarding_queue], + "doors": self.unit.doors, + "door_positions": self.unit.door_positions, + "board_positions": self.unit.board_positions, + "door_argument_nos": self.unit.door_argument_nos, + "door_open_thresholds": self.unit.door_open_thresholds, + "is_rear_loader": self.unit.is_rear_loader, + "will_disembark": self.unit.will_disembark + } + + def set_as_transport(self): + self.unit.is_transport = True + if self.unit.name in transport_helicopters: + if self.unit.name == "UH-1H": + self.unit.max_capacity = transport_helicopters["UH-1H"]["max_capacity"] + self.unit.max_embark_range = transport_helicopters["UH-1H"]["max_embark_range"] + self.unit.boarding_distance = transport_helicopters["UH-1H"]["boarding_distance"] + self.unit.current_capacity = 0 + self.unit.current_cargo_weight = 0 + self.unit.unit_array = [] + self.unit.en_boarding_queue = [] + self.unit.doors = transport_helicopters["UH-1H"]["doors"] + self.unit.door_positions = transport_helicopters["UH-1H"]["door_positions"] + self.unit.board_positions = transport_helicopters["UH-1H"]["board_positions"] + + self.unit.door_argument_nos = transport_helicopters["UH-1H"]["door_argument_nos"] + self.unit.will_disembark = False + self.unit.register_draw_argument(43) #Register draw argument 43 for UH-1H + self.unit.register_draw_argument(44) + self.unit.door_open_thresholds = transport_helicopters["UH-1H"]["door_open_thresholds"] + self.unit.is_rear_loader = transport_helicopters["UH-1H"]["is_rear_loader"] + else: + self.unit.max_capacity = 8 + self.unit.max_embark_range = 100 + self.unit.boarding_distance = 5 + self.unit.current_capacity = 0 + self.unit.current_cargo_weight = 0 + self.unit.unit_array = [] + self.unit.en_boarding_queue = [] + self.unit.doors = 1 + self.unit.door_positions = [(5,pi),(0,0)] + self.unit.board_positions = [(15,pi),(0,0)] + self.unit.door_argument_nos = None + self.unit.door_open_thresholds = None + self.unit.will_disembark = False + self.unit.is_rear_loader = True + + elif self.unit.name in transport_ground: + if self.unit.name == "M-113": + self.unit.max_capacity = transport_ground["M-113"]["max_capacity"] + self.unit.max_embark_range = transport_ground["M-113"]["max_embark_range"] + self.unit.boarding_distance = transport_ground["M-113"]["boarding_distance"] + self.unit.current_capacity = 0 + self.unit.current_cargo_weight = 0 + self.unit.unit_array = [] + self.unit.en_boarding_queue = [] + self.unit.doors = transport_ground["M-113"]["doors"] + self.unit.door_positions = transport_ground["M-113"]["door_positions"] + self.unit.board_positions = transport_ground["M-113"]["board_positions"] + self.unit.door_argument_nos = transport_ground["M-113"]["door_argument_nos"] + self.unit.door_open_thresholds = transport_ground["M-113"]["door_open_thresholds"] + self.unit.will_disembark = False + self.unit.is_rear_loader = transport_ground["M-113"]["is_rear_loader"] + else: + self.unit.max_capacity = 4 + self.unit.max_embark_range = 50 + self.unit.boarding_distance = 5 + self.unit.current_capacity = 0 + self.unit.current_cargo_weight = 0 + self.unit.unit_array = [] + self.unit.en_boarding_queue = [] + self.unit.doors = 1 + self.unit.door_positions = [(5,pi),(0,0)] + self.unit.board_positions = [(15,pi),(0,0)] + self.unit.door_argument_nos = None + self.unit.door_open_thresholds = None + self.unit.will_disembark = False + self.unit.is_rear_loader = True + + logger.info(f"Set unit '{self.unit.name}' as transport, with {self.unit.current_capacity} / {self.unit.max_capacity}.") + +class DisembarkedInfantry(Unit): + def __str__(self): + return f"DisembarkedInfrantry(unit_id={self.unit_id}, group_id={self.group_id}, position={self.position}, heading={self.heading})" + + def __init__(self, Unit): + self.unit = Unit + + def disembark_from_transport(self): + destination = self.position.project_with_bearing_and_distance(30, self.heading) + # Set the destination for the unit + self.set_roe(4) #set to hold fire to avoid stopping to shoot + self.is_loadable = False + self.set_path([destination]) + if self.check_for_enemy_in_range(): + self.set_speed(10) + else: + self.set_speed(3) + self.register_on_destination_reached_callback( + self.on_destination_reached, + destination, + threshold=15.0, + timeout=30.0 # Timeout after 30 seconds if the destination is not reached + ) + + def check_for_enemy_in_range(self): + units = api.get_units() + for unit in units.values(): + if unit.alive and unit.coalition != self.coalition: + distance_to_enemy = self.position.distance_to(unit.position) + if distance_to_enemy < 2000: #if an enemy is within 100m + return True + return False + + async def on_destination_reached(self, _, reached: bool): + if not reached: + # logger.info(f"Unit {self} did not reach its destination.") + self.set_roe(1) + new_patrol = self.position.project_with_bearing_and_distance(1000, self.transport_spawn_heading) + await asyncio.sleep(self.time_delay) #wait a bit before trying again + self.set_path([new_patrol]) + if self.check_for_enemy_in_range(): + self.set_speed(10) + else: + self.set_speed(1.3) + await asyncio.sleep(before_can_re_embark_time) #wait before setting to be boardable + self.is_loadable = True + logger.info(f"Unit {self} is now boardable again.") + else: + self.set_roe(1) + logger.info(f"Unit {self} has reached its destination.") + new_patrol = self.position.project_with_bearing_and_distance(1000, self.transport_spawn_heading) + await asyncio.sleep(self.time_delay) #wait a bit before trying again + self.set_path([new_patrol]) + if self.check_for_enemy_in_range(): + self.set_speed(10) + else: + self.set_speed(1.3) + await asyncio.sleep(before_can_re_embark_time) #wait before setting to be boardable + self.is_loadable = True + logger.info(f"Unit {self} is now boardable again.") + + +class Embarker(Unit): + def __str__(self): + return f"DisembarkedInfrantry(unit_id={self.unit_id}, group_id={self.group_id}, position={self.position}, heading={self.heading})" + + def __init__(self, Unit): + self.unit = Unit + + def to_json(self): + return { + "is_embarker": self.unit.is_embarker, + "is_moving": self.unit.is_moving, + "is_loadable": self.unit.is_loadable, + "in_embark_queue": self.unit.in_embark_queue if hasattr(self.unit, 'in_embark_queue') else False, + "transport_unit": self.unit.transport_unit.ID if hasattr(self.unit, 'transport_unit') and self.unit.transport_unit else None + } + + def set_as_embarker(self): + self.unit.is_embarker = True + self.unit.is_moving = False + self.unit.is_loadable = True + logger.info(f"Set unit '{self.unit.name}' as embarker.") + self.unit.set_custom_string("I am an embarker.") + + def can_board(self): + transport = self.transport_unit + if transport.current_capacity < transport.max_capacity: + transport.unit_array.append(self.name) + transport.current_capacity += 1 + self.delete_unit() + else: + pass + + def board_transport(self): + door, num_doors_open = self.get_closest_door() + if num_doors_open > 1: door_bypass = True + else: door_bypass = False + + if door is None: + pass + elif door is not None: + if self.is_moving: + pass + elif not self.is_moving: + distance_to_door = self.position.distance_to(door) + distance_to_centre = self.position.distance_to(self.transport_unit.position) + if distance_to_door < distance_to_centre: + bearing = self.position.bearing_to(door) + if hasattr(self,'nudge'): + nudge_factor = self.nudge + else: + nudge_factor = 0 + destination = self.position.project_with_bearing_and_distance(distance_to_door+nudge_factor, bearing) + destination.threshold = 2 + # Set the destination for the unit + self.set_path([destination]) + self.register_on_destination_reached_callback( + self.on_destination_reached, + destination, + threshold=2.0, + timeout=10.0 # Timeout after 30 seconds if the destination is not reached + ) + self.is_moving = True + else:# distance_to_door >= distance_to_centre: + if self.transport_unit.is_rear_loader: + in_front_of_transport = self.transport_unit.position.project_with_bearing_and_distance(15, self.transport_unit.heading-pi) + else: + in_front_of_transport = self.transport_unit.position.project_with_bearing_and_distance(15, self.transport_unit.heading) + bearing = self.position.bearing_to(in_front_of_transport) + destination = self.position.project_with_bearing_and_distance(distance_to_door, bearing) + destination.threshold = 2 + self.set_path([destination]) + self.register_on_destination_reached_callback( + self.on_destination_reached, + destination, + threshold=2.0, + timeout=10.0 + ) + self.is_moving = True + + def get_closest_door(self): + return check_closest_open_door(self.transport_unit, self) + + async def on_destination_reached(self, _, reached: bool): + if not reached: + logger.info(f"Unit {self} did not reach its destination.") + self.is_moving = False + else: + logger.info(f"Unit {self} has reached its destination.") + self.is_moving = False + + await asyncio.sleep(10) + self.board_transport() # Attempt to board again + +def check_closest_open_door(transport, embarker): + if transport.name in transport_helicopters: + if transport.door_argument_nos is None and transport.doors > 0: + return transport.position.project_with_bearing_and_distance(5,transport.heading + pi), transport.heading + pi + elif transport.door_argument_nos is not None and transport.doors > 0: + closest_door = None + doors_open = 0 + distance_to_closest_door = float('inf') + for i in range(transport.doors): + if transport.draw_arguments[i].value >= transport.door_open_thresholds[i]: + doors_open += 1 + distance = embarker.position.distance_to(transport.position.project_with_bearing_and_distance(transport.door_positions[i*2][0], transport.heading + transport.door_positions[i*2][1]).project_with_bearing_and_distance(transport.door_positions[i*2+1][0], transport.heading + transport.door_positions[i*2+1][1])) + if distance < distance_to_closest_door: + distance_to_closest_door = distance + closest_door = transport.position.project_with_bearing_and_distance(transport.door_positions[i*2][0], transport.heading + transport.door_positions[i*2][1]).project_with_bearing_and_distance(transport.door_positions[i*2+1][0], transport.heading + transport.door_positions[i*2+1][1]) + return closest_door, doors_open + else: + return None, 0 + elif transport.name in transport_ground: + if transport.door_argument_nos is None and transport.doors > 0: + return transport.position.project_with_bearing_and_distance(2,transport.heading + pi), transport.heading + pi + elif transport.door_argument_nos is not None and transport.doors > 0: + closest_door = None + doors_open = 0 + distance_to_closest_door = float('inf') + for i in range(transport.doors): + if transport.draw_arguments[i].value >= transport.door_open_thresholds[i]: + doors_open += 1 + distance = embarker.position.distance_to(transport.position.project_with_bearing_and_distance(transport.door_positions[i*2][0], transport.heading + transport.door_positions[i*2][1]).project_with_bearing_and_distance(transport.door_positions[i*2+1][0], transport.heading + transport.door_positions[i*2+1][1])) + if distance < distance_to_closest_door: + distance_to_closest_door = distance + closest_door = transport.position.project_with_bearing_and_distance(transport.door_positions[i*2][0], transport.heading + transport.door_positions[i*2][1]).project_with_bearing_and_distance(transport.door_positions[i*2+1][0], transport.heading + transport.door_positions[i*2+1][1]) + return closest_door, doors_open + else: + return None, 0 + +def check_for_door_status(transporter): + if transporter.name in transport_helicopters: + if transporter.door_argument_nos is None and transporter.doors > 0: + return True + elif transporter.door_argument_nos is not None and transporter.doors > 0: + a_door_is_open = False + for i in range(transporter.doors): + if transporter.draw_arguments[i].value >= transporter.door_open_thresholds[i]: + a_door_is_open = True + return a_door_is_open + else: + return False + elif transporter.name in transport_ground: + if transporter.door_argument_nos is None and transporter.doors > 0: + return True + elif transporter.door_argument_nos is not None and transporter.doors > 0: + a_door_is_open = False + for i in range(transporter.doors): + if transporter.draw_arguments[i].value >= transporter.door_open_thresholds[i]: + a_door_is_open = True + return a_door_is_open + else: + return False + +async def load_loadable_units(): + units = api.get_units() + for embarker in units.values(): + if embarker.alive and hasattr(embarker, 'is_embarker'): + if hasattr(embarker, 'in_embark_queue') and hasattr(embarker, 'transport_unit') and hasattr(embarker, 'is_moving'): + if embarker.transport_unit.name in transport_types: + #check the speed and distance, slow down if close + distance_to_transport = embarker.position.distance_to(embarker.transport_unit.position) + if distance_to_transport > 10 and embarker.speed < 1.4: + embarker.set_speed(10) + elif distance_to_transport < 10 and embarker.speed >= 3: + embarker.set_speed(2) + elif distance_to_transport < 5 and embarker.speed >= 1.3: + embarker.set_speed(1.3) + if embarker.roe != "hold": + embarker.set_roe(4) #set to hold fire to avoid stopping to shoot + #check the doors are open + if check_for_door_status(embarker.transport_unit): + closest_door, num_doors_open = check_closest_open_door(embarker.transport_unit, embarker) + if closest_door is not None: + #print(f"A door is open on {embarker.transport_unit.name}, closest door is {closest_door}, {num_doors_open} doors open") + embarker.__class__ = Embarker + #check if close enough to board + closest_door, _ = embarker.get_closest_door() + door_distance = embarker.position.distance_to(closest_door) + if door_distance < embarker.transport_unit.boarding_distance: + transport = embarker.transport_unit + embarker_units = [ + (embarker, embarker.position.distance_to(transport.position)) + for embarker in units.values() + if embarker.alive + and hasattr(embarker, 'is_embarker') + and embarker.position.distance_to(transport.position) < transport.boarding_distance + ] + + embarkers_sorted = sorted(embarker_units, key=lambda x: x[1]) + if not embarkers_sorted: + pass + else: + if embarker.ID == embarkers_sorted[0][0].ID: + transport.current_capacity += 1 + transport.unit_array.append(embarker) + transport.set_cargo_weight(transport.current_cargo_weight + 100) #assume 100kg per infantry with kit + transport.current_cargo_weight += 100 + embarker.delete_unit() + asyncio.create_task(set_as_disembarking(transport)) + break + #else run it closer + if embarker.is_moving: + if hasattr(embarker, 'last_pos'): + if embarker.position == embarker.last_pos: + embarker.is_moving = False + embarker.set_speed(1.3) + if hasattr(embarker, 'nudge'): + embarker.nudge = embarker.nudge + 2 + else: + embarker.nudge = 2 + embarker.last_pos = embarker.position + pass + elif not embarker.is_moving: + embarker.board_transport() + else: + #no doors so do nothing + pass + +def generate_transport_units(): + units = api.get_units() + for unit in units.values(): + if unit.alive and unit.name in transport_types and not hasattr(unit, 'is_transport'): + new_transport = Transporter(unit) + new_transport.set_as_transport() + + elif unit.alive and unit.name in embarker_types and not hasattr(unit, 'is_embarker'): + new_emabarquee = Embarker(unit) + new_emabarquee.set_as_embarker() + +async def set_as_disembarking(transport): + await asyncio.sleep(alternate_time) + transport.will_disembark = True + +async def set_as_not_disembarking(transport): + await asyncio.sleep(alternate_time) + transport.will_disembark = False + +unload_semaphore = Semaphore(1) + +async def check_for_unloadable_units(): + # Use the semaphore to ensure only one instance runs at a time + async with unload_semaphore: + units = api.get_units() + try: + for transporter in units.values(): + if transporter.alive and hasattr(transporter, 'is_transport') and transporter.will_disembark: + # Check if the transporter is in a position to disembark units + if transporter.speed < 2 and check_for_door_status(transporter) and not transporter.airborne: # check speed is less than 2 m/s and doors are open + first_two_spawns = True # Track if we are handling the first two spawns + to_remove = [] #sets up variable to hold units to remove from queue + for disembarker in transporter.unit_array: + # Get the open doors + open_doors = [] + open_doors_headings = [] + for i in range(transporter.doors): + if transporter.draw_arguments[i].value >= transporter.door_open_thresholds[i]: + door_position = transporter.position.project_with_bearing_and_distance( + transporter.door_positions[i * 2][0], + transporter.heading + transporter.door_positions[i * 2][1] + ).project_with_bearing_and_distance( + transporter.door_positions[i * 2 + 1][0], + transporter.heading + transporter.door_positions[i * 2 + 1][1] + ) + door_heading = transporter.heading + transporter.door_positions[i * 2][1] + open_doors.append(door_position) + open_doors_headings.append(door_heading) + + # Round-robin spawn mechanism + if not hasattr(transporter, 'last_door_index'): + transporter.last_door_index = 0 # Initialize the last used door index + + # Get the next door in the round-robin sequence + door_index = transporter.last_door_index % len(open_doors) + transporter.last_door_index += 1 # Increment the door index for the next spawn + + # Spawn the unit at the selected door + door_position = open_doors[door_index] + door_heading = open_doors_headings[door_index] + + spawn_table: UnitSpawnTable = UnitSpawnTable( + unit_type=disembarker.name, + location=door_position, + heading=door_heading, + skill="High", + livery_id="" + ) + + async def execution_callback(new_group_ID: int): + logger.info(f"New units spawned, groupID: {new_group_ID}") + units = api.get_units() + for new_unit in units.values(): + if new_unit.group_id == new_group_ID: + logger.info(f"New unit spawned: {new_unit}") + new_unit.__class__ = DisembarkedInfantry + new_unit.transport_spawn_heading = transporter.heading + new_unit.disembark_from_transport() + new_unit.original_position = new_unit.position + #the delay is a function of how many units are left to disembark and how long it takes to get to the disembark spot + new_unit.time_delay = transporter.max_capacity*2 - transporter.current_capacity # Random delay between 10 and 30 seconds + + api.spawn_ground_units([spawn_table], transporter.coalition, "", True, 0, execution_callback) + to_remove.append(disembarker) + transporter.en_boarding_queue = [] + transporter.current_capacity -= 1 + transporter.set_cargo_weight(transporter.current_cargo_weight - 100) # Assume 100kg per infantry with kit + transporter.current_cargo_weight -= 100 + + # Add a delay between spawns + if len(open_doors) > 1 and first_two_spawns: + # Shorter delay for the first two spawns if both doors are open + await asyncio.sleep(0.5) + first_two_spawns = False + else: + # Normal delay for subsequent spawns or single-door spawns + await asyncio.sleep(2.5) + for disembarker in to_remove: + transporter.unit_array.remove(disembarker) + if transporter.current_capacity == 0: + await set_as_not_disembarking(transporter) + + logger.info(f"Spawned unit '{disembarker.name}' from open door of transport '{transporter.name}'.") + except Exception as e: + logger.error(f"Error in check_for_unloadable_units: {e}") + +def check_for_loadable_units(): + units = api.get_units() + for transporter in units.values(): + if transporter.alive and hasattr(transporter, 'is_transport') and not transporter.will_disembark: + if len(transporter.unit_array) < transporter.max_capacity: + if transporter.speed < 2 and check_for_door_status(transporter): #check speed is less than 2 m/s and doors are open + # print("Speed is okay") + embarker_units = [ + (embarker, embarker.position.distance_to(transporter.position)) + for embarker in units.values() + if embarker.alive + and hasattr(embarker, 'is_embarker') + and getattr(embarker, 'is_loadable', True) # Check if is_loadable is True + and embarker.position.distance_to(transporter.position) < transporter.max_embark_range + ] + if embarker_units is None or len(embarker_units) == 0: + continue + else: + for embarker in embarker_units: + if hasattr(embarker, 'in_embark_queue') and embarker.in_embark_queue: + if embarker.in_embark_queue: + embarker_units.remove(embarker) + + embarkers_sorted = sorted(embarker_units, key=lambda x: x[1]) + closest_embarkers = embarkers_sorted[:transporter.max_capacity-len(transporter.en_boarding_queue)] + + for embarker, distance in closest_embarkers: + if embarker not in transporter.en_boarding_queue and distance < transporter.max_embark_range: + transporter.en_boarding_queue.append(embarker) + embarker.in_embark_queue = True + embarker.transport_unit = transporter + logger.info(f"Added embarker '{embarker.name}' to '{transporter.name}' s boarding queue.") + elif embarker in transporter.en_boarding_queue: + pass + else: + pass #we pass as the transport is full + + +############# +#API SECTION# +############# +def on_api_startup(api: API): + global units_to_delete + logger.info("API started") + + # Get all the units from the API. Force an update to get the latest units. + units = api.update_units() + + # Initialize the list to hold units to delete + units_to_delete = [] + +def on_unit_alive_change(unit: Unit, value: bool): + global units_to_delete + + if units_to_delete is None: + logger.error("units_to_delete is not initialized.") + return + + # Check if the unit has been deleted + if value is False: + if unit in units_to_delete: + units_to_delete.remove(unit) + else: + pass + +async def update_data(): + units = api.get_units() + for unit in units.values(): + if unit.alive and hasattr(unit, 'is_transport'): + stringified_json = json.dumps(Transporter(unit).to_json()) + unit.set_custom_string(stringified_json) + elif unit.alive and hasattr(unit, 'is_embarker'): + stringified_json = json.dumps(Embarker(unit).to_json()) + unit.set_custom_string(stringified_json) + await asyncio.sleep(1) + +async def on_api_update(api: API): + generate_transport_units() + check_for_loadable_units() + asyncio.create_task(load_loadable_units()) + asyncio.create_task(check_for_unloadable_units()) + asyncio.create_task(update_data()) + +if __name__ == "__main__": + api = API() + api.register_on_update_callback(on_api_update) + api.register_on_startup_callback(on_api_startup) + api.run() \ No newline at end of file diff --git a/scripts/python/API/olympus.json b/scripts/python/API/olympus.json index b81d60e5..7159c6ee 100644 --- a/scripts/python/API/olympus.json +++ b/scripts/python/API/olympus.json @@ -4,7 +4,7 @@ "port": 4512 }, "authentication": { - "gameMasterPassword": "a00a5973aacb17e4659125fbe10f4160d096dd84b2f586d2d75669462a30106d", + "gameMasterPassword": "a474219e5e9503c84d59500bb1bda3d9ade81e52d9fa1c234278770892a6dd74", "blueCommanderPassword": "7d2e1ef898b21db7411f725a945b76ec8dcad340ed705eaf801bc82be6fe8a4a", "redCommanderPassword": "abc5de7abdb8ed98f6d11d22c9d17593e339fde9cf4b9e170541b4f41af937e3" }, diff --git a/scripts/python/API/unit/unit.py b/scripts/python/API/unit/unit.py index dc8e1d0f..4cc43463 100644 --- a/scripts/python/API/unit/unit.py +++ b/scripts/python/API/unit/unit.py @@ -83,6 +83,8 @@ class Unit: self.acquisition_range = 0.0 self.cargo_weight = 0.0 self.draw_arguments: List[DrawArgument] = [] + self.custom_string = "" + self.custom_integer = 0 self.previous_total_ammo = 0 self.total_ammo = 0 @@ -670,6 +672,20 @@ class Unit: # Trigger callbacks for property change if "draw_arguments" in self.on_property_change_callbacks: self._trigger_callback("draw_arguments", self.draw_arguments) + elif datum_index == DataIndexes.CUSTOM_STRING.value: + custom_string = data_extractor.extract_string() + if custom_string != self.custom_string: + self.custom_string = custom_string + # Trigger callbacks for property change + if "custom_string" in self.on_property_change_callbacks: + self._trigger_callback("custom_string", self.custom_string) + elif datum_index == DataIndexes.CUSTOM_INTEGER.value: + custom_integer = data_extractor.extract_uint32() + if custom_integer != self.custom_integer: + self.custom_integer = custom_integer + # Trigger callbacks for property change + if "custom_integer" in self.on_property_change_callbacks: + self._trigger_callback("custom_integer", self.custom_integer) # --- API functions requiring ID --- def set_path(self, path: List[LatLng]): @@ -778,4 +794,10 @@ class Unit: return self.api.send_command({"setCargoWeight": {"ID": self.ID, "weight": cargo_weight}}) def register_draw_argument(self, argument: int, active: bool = True): - return self.api.send_command({"registerDrawArgument": {"ID": self.ID, "argument": argument, "active": active}}) \ No newline at end of file + return self.api.send_command({"registerDrawArgument": {"ID": self.ID, "argument": argument, "active": active}}) + + def set_custom_string(self, custom_string: str): + return self.api.send_command({"setCustomString": {"ID": self.ID, "customString": custom_string}}) + + def set_custom_integer(self, custom_integer: int): + return self.api.send_command({"setCustomInteger": {"ID": self.ID, "customInteger": custom_integer}}) \ No newline at end of file