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 69771271..95c4c477 100644 --- a/backend/core/src/unit.cpp +++ b/backend/core/src/unit.cpp @@ -346,6 +346,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 13383785..27dd1c16 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