diff --git a/backend/core/include/airunit.h b/backend/core/include/airunit.h index 66610466..ce559b68 100644 --- a/backend/core/include/airunit.h +++ b/backend/core/include/airunit.h @@ -22,6 +22,8 @@ public: virtual void setRacetrackLength(double newValue); virtual void setRacetrackAnchor(Coords newValue); virtual void setRacetrackBearing(double newValue); + + virtual void setCargoWeight(double newValue); protected: virtual void AIloop(); diff --git a/backend/core/include/commands.h b/backend/core/include/commands.h index 5119a9eb..b4fa3e68 100644 --- a/backend/core/include/commands.h +++ b/backend/core/include/commands.h @@ -538,4 +538,44 @@ public: private: const unsigned int spotID; const Coords destination; +}; + +/* Set cargo weight */ +class SetCargoWeight : public Command +{ + public: + SetCargoWeight(unsigned int ID, double weight, function callback = []() {}) : + Command(callback), + ID(ID), + weight(weight) + { + priority = CommandPriority::LOW; + }; + virtual string getString(); + virtual unsigned int getLoad() { return 5; } + +private: + const unsigned int ID; + const double weight; +}; + +/* Register draw argument */ +class RegisterDrawArgument : public Command +{ + public: + RegisterDrawArgument(unsigned int ID, unsigned int argument, bool active, function callback = []() {}) : + Command(callback), + ID(ID), + argument(argument), + active(active) + { + priority = CommandPriority::LOW; + }; + virtual string getString(); + virtual unsigned int getLoad() { return 5; } + +private: + const unsigned int ID; + const unsigned int argument; + const bool active; }; \ No newline at end of file diff --git a/backend/core/include/datatypes.h b/backend/core/include/datatypes.h index d4a9a3b0..fb52e979 100644 --- a/backend/core/include/datatypes.h +++ b/backend/core/include/datatypes.h @@ -70,6 +70,8 @@ namespace DataIndex { aimMethodRange, acquisitionRange, airborne, + cargoWeight, + drawArguments, lastIndex, endOfData = 255 }; @@ -159,6 +161,11 @@ namespace DataTypes { unsigned int ID = 0; unsigned char detectionMethod = 0; }; + + struct DrawArgument { + unsigned int argument = 0; + double value = 0.0; + }; } #pragma pack(pop) @@ -167,6 +174,7 @@ bool operator==(const DataTypes::Radio& lhs, const DataTypes::Radio& rhs); bool operator==(const DataTypes::GeneralSettings& lhs, const DataTypes::GeneralSettings& rhs); bool operator==(const DataTypes::Ammo& lhs, const DataTypes::Ammo& rhs); bool operator==(const DataTypes::Contact& lhs, const DataTypes::Contact& rhs); +bool operator==(const DataTypes::DrawArgument& lhs, const DataTypes::DrawArgument& rhs); struct SpawnOptions { string unitType; diff --git a/backend/core/include/scheduler.h b/backend/core/include/scheduler.h index 304a9da1..c64045ec 100644 --- a/backend/core/include/scheduler.h +++ b/backend/core/include/scheduler.h @@ -19,6 +19,7 @@ public: return true; } } + return false; } void setFrameRate(double newFrameRate) { frameRate = newFrameRate; } diff --git a/backend/core/include/unit.h b/backend/core/include/unit.h index 0ff980ba..fad95eee 100644 --- a/backend/core/include/unit.h +++ b/backend/core/include/unit.h @@ -130,9 +130,11 @@ public: virtual void setAcquisitionRange(double newValue) { updateValue(acquisitionRange, newValue, DataIndex::acquisitionRange); } virtual void setRadarState(bool newValue) { updateValue(radarState, newValue, DataIndex::radarState); } 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); /********** Getters **********/ - virtual string getCategory() { return category; }; + virtual string getCategory() { return category; } virtual bool getAlive() { return alive; } virtual unsigned char getAlarmState() { return alarmState; } virtual bool getHuman() { return human; } @@ -197,6 +199,8 @@ public: virtual double getAcquisitionRange() { return acquisitionRange; } virtual bool getRadarState() { return radarState; } virtual bool getAirborne() { return airborne; } + virtual double getCargoWeight() { return cargoWeight; } + virtual vector getDrawArguments() { return drawArguments; } protected: unsigned int ID; @@ -267,6 +271,8 @@ protected: double aimMethodRange = 0; double acquisitionRange = 0; bool airborne = false; + double cargoWeight = 0; + vector drawArguments; /********** Other **********/ unsigned int taskCheckCounter = 0; diff --git a/backend/core/src/airunit.cpp b/backend/core/src/airunit.cpp index baa33bb0..d41941e4 100644 --- a/backend/core/src/airunit.cpp +++ b/backend/core/src/airunit.cpp @@ -428,4 +428,14 @@ void AirUnit::setRacetrackBearing(double newRacetrackBearing) { triggerUpdate(DataIndex::racetrackBearing); } +} + +void AirUnit::setCargoWeight(double newCargoWeight) { + if (cargoWeight != newCargoWeight) { + cargoWeight = newCargoWeight; + triggerUpdate(DataIndex::cargoWeight); + + Command* command = dynamic_cast(new SetCargoWeight(this->ID, cargoWeight)); + scheduler->appendCommand(command); + } } \ No newline at end of file diff --git a/backend/core/src/commands.cpp b/backend/core/src/commands.cpp index b0b0c93d..75f1cf65 100644 --- a/backend/core/src/commands.cpp +++ b/backend/core/src/commands.cpp @@ -318,4 +318,27 @@ string DeleteSpot::getString() commandSS << "Olympus.deleteSpot, " << spotID; return commandSS.str(); +} + +/* SetCargoWeight command */ +string SetCargoWeight::getString() +{ + std::ostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.setCargoWeight, " + << ID << ", " + << weight; + return commandSS.str(); +} + +/* RegisterDrawArgument command */ +string RegisterDrawArgument::getString() +{ + std::ostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.registerDrawArgument, " + << ID << ", " + << argument << ", " + << active; + return commandSS.str(); } \ No newline at end of file diff --git a/backend/core/src/datatypes.cpp b/backend/core/src/datatypes.cpp index eb0c52cd..9ea68425 100644 --- a/backend/core/src/datatypes.cpp +++ b/backend/core/src/datatypes.cpp @@ -12,19 +12,24 @@ bool operator==(const DataTypes::Radio& lhs, const DataTypes::Radio& rhs) bool operator==(const DataTypes::GeneralSettings& lhs, const DataTypes::GeneralSettings& rhs) { - return lhs.prohibitAA == rhs.prohibitAA && lhs.prohibitAfterburner == rhs.prohibitAfterburner && lhs.prohibitAG == rhs.prohibitAG && + return lhs.prohibitAA == rhs.prohibitAA && lhs.prohibitAfterburner == rhs.prohibitAfterburner && lhs.prohibitAG == rhs.prohibitAG && lhs.prohibitAirWpn == rhs.prohibitAirWpn && lhs.prohibitJettison == rhs.prohibitJettison; } bool operator==(const DataTypes::Ammo& lhs, const DataTypes::Ammo& rhs) { - return lhs.category == rhs.category && lhs.guidance == rhs.guidance && lhs.missileCategory == rhs.missileCategory && + return lhs.category == rhs.category && lhs.guidance == rhs.guidance && lhs.missileCategory == rhs.missileCategory && lhs.quantity == rhs.quantity && strcmp(lhs.name, rhs.name) == 0; } +bool operator==(const DataTypes::DrawArgument& lhs, const DataTypes::DrawArgument& rhs) +{ + return lhs.argument == rhs.argument && lhs.value == rhs.value; +} + bool operator==(const DataTypes::Contact& lhs, const DataTypes::Contact& rhs) { - return lhs.detectionMethod == rhs.detectionMethod && lhs.ID == rhs.ID; + return lhs.detectionMethod == rhs.detectionMethod && lhs.ID == rhs.ID; } diff --git a/backend/core/src/scheduler.cpp b/backend/core/src/scheduler.cpp index 2bdb69cc..8d17619d 100644 --- a/backend/core/src/scheduler.cpp +++ b/backend/core/src/scheduler.cpp @@ -168,6 +168,12 @@ void Scheduler::handleRequest(string key, json::value value, string username, js string WP = to_string(i); double lat = path[i][L"lat"].as_double(); double lng = path[i][L"lng"].as_double(); + if (path[i].has_number_field(L"threshold")) { + double threshold = path[i][L"threshold"].as_double(); + Coords dest; dest.lat = lat; dest.lng = lng; dest.threshold = threshold; + newPath.push_back(dest); + continue; + } Coords dest; dest.lat = lat; dest.lng = lng; newPath.push_back(dest); } @@ -821,6 +827,31 @@ void Scheduler::handleRequest(string key, json::value value, string username, js unitsManager->loadDatabases(); } /************************/ + else if (key.compare("setCargoWeight") == 0) + { + unsigned int ID = value[L"ID"].as_integer(); + Unit* unit = unitsManager->getUnit(ID); + if (unit != nullptr) { + double weight = value[L"weight"].as_double(); + unit->setCargoWeight(weight); + log(username + " set weight to unit " + unit->getUnitName() + "(" + unit->getName() + "), " + to_string(weight), true); + } + } + /************************/ + else if (key.compare("registerDrawArgument") == 0) + { + unsigned int ID = value[L"ID"].as_integer(); + Unit* unit = unitsManager->getUnit(ID); + if (unit != nullptr) { + int argument = value[L"argument"].as_integer(); + bool active = value[L"active"].as_bool(); + + command = dynamic_cast(new RegisterDrawArgument(ID, argument, active)); + + log(username + " registered draw argument " + to_string(argument) + " for unit " + unit->getUnitName() + "(" + unit->getName() + "), value:" + to_string(active), true); + } + } + /************************/ else { log("Unknown command: " + key); diff --git a/backend/core/src/unit.cpp b/backend/core/src/unit.cpp index 359b4168..69771271 100644 --- a/backend/core/src/unit.cpp +++ b/backend/core/src/unit.cpp @@ -128,6 +128,20 @@ void Unit::update(json::value json, double dt) setAmmo(ammo); } + if (json.has_object_field(L"drawArguments")) { + vector drawArguments; + for (auto const& el : json[L"drawArguments"].as_object()) { + DataTypes::DrawArgument drawArgumentItem; + auto drawArgumentJson = el.second; + if (drawArgumentJson.has_number_field(L"argument")) + drawArgumentItem.argument = drawArgumentJson[L"argument"].as_number().to_uint32(); + if (drawArgumentJson.has_number_field(L"value")) + drawArgumentItem.value = drawArgumentJson[L"value"].as_number().to_double(); + drawArguments.push_back(drawArgumentItem); + } + setDrawArguments(drawArguments); + } + if (json.has_object_field(L"contacts")) { vector contacts; for (auto const& el : json[L"contacts"].as_object()) { @@ -330,6 +344,8 @@ void Unit::getData(stringstream& ss, unsigned long long time) case DataIndex::aimMethodRange: appendNumeric(ss, datumIndex, aimMethodRange); break; case DataIndex::acquisitionRange: appendNumeric(ss, datumIndex, acquisitionRange); break; case DataIndex::airborne: appendNumeric(ss, datumIndex, airborne); break; + case DataIndex::cargoWeight: appendNumeric(ss, datumIndex, cargoWeight); break; + case DataIndex::drawArguments: appendVector(ss, datumIndex, drawArguments); break; } } } @@ -701,6 +717,24 @@ void Unit::setGeneralSettings(DataTypes::GeneralSettings newGeneralSettings, boo } } +void Unit::setDrawArguments(vector newDrawArguments) +{ + if (drawArguments.size() == newDrawArguments.size()) { + bool equal = true; + for (int i = 0; i < drawArguments.size(); i++) { + if (drawArguments.at(i) != newDrawArguments.at(i)) + { + equal = false; + break; + } + } + if (equal) + return; + } + drawArguments = newDrawArguments; + triggerUpdate(DataIndex::drawArguments); +} + void Unit::setDesiredSpeed(double newDesiredSpeed) { if (desiredSpeed != newDesiredSpeed) { @@ -767,6 +801,7 @@ void Unit::goToDestination(string enrouteTask) } } +// NOTE: if the current active path has a threshold set, that value will be used instead of the passed one bool Unit::isDestinationReached(double threshold) { if (activeDestination != NULL) @@ -776,7 +811,7 @@ bool Unit::isDestinationReached(double threshold) { double dist = 0; Geodesic::WGS84().Inverse(p->getPosition().lat, p->getPosition().lng, activeDestination.lat, activeDestination.lng, dist); - if (dist < threshold) + if (dist < (activeDestination.threshold == 0? threshold: activeDestination.threshold)) { log(unitName + " destination reached"); return true; diff --git a/backend/utils/include/utils.h b/backend/utils/include/utils.h index 16b16c68..70f8cf67 100644 --- a/backend/utils/include/utils.h +++ b/backend/utils/include/utils.h @@ -6,6 +6,7 @@ struct Coords { double lat = 0; double lng = 0; double alt = 0; + double threshold = 0; // used for proximity checks only, not part of the actual coordinates }; struct Offset { diff --git a/backend/utils/src/utils.cpp b/backend/utils/src/utils.cpp index a0ebfdf7..d2097a39 100644 --- a/backend/utils/src/utils.cpp +++ b/backend/utils/src/utils.cpp @@ -64,9 +64,9 @@ std::string random_string(size_t length) return str; } -bool operator== (const Coords& a, const Coords& b) { return a.lat == b.lat && a.lng == b.lng && a.alt == b.alt; } +bool operator== (const Coords& a, const Coords& b) { return a.lat == b.lat && a.lng == b.lng && a.alt == b.alt && a.threshold == b.threshold; } bool operator!= (const Coords& a, const Coords& b) { return !(a == b); } -bool operator== (const Coords& a, const double& b) { return a.lat == b && a.lng == b && a.alt == b; } +bool operator== (const Coords& a, const double& b) { return a.lat == b && a.lng == b && a.alt == b && a.threshold == b; } bool operator!= (const Coords& a, const double& b) { return !(a == b); } bool operator== (const Offset& a, const Offset& b) { return a.x == b.x && a.y == b.y && a.z == b.z; } diff --git a/frontend/react/.vscode/tasks.json b/frontend/react/.vscode/tasks.json index 586890ef..fb796833 100644 --- a/frontend/react/.vscode/tasks.json +++ b/frontend/react/.vscode/tasks.json @@ -1,12 +1,6 @@ { "version": "2.0.0", "tasks": [ - { - "label": "check-setup", - "type": "shell", - "command": "cd .. ; ./check_setup.bat", - "isBackground": false - }, { "type": "npm", "script": "dev", diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index b2c39e16..e95c7267 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -547,6 +547,8 @@ export enum DataIndexes { aimMethodRange, acquisitionRange, airborne, + cargoWeight, + drawingArguments, endOfData = 255, } diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 18ccf276..13383785 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -219,6 +219,11 @@ export interface Offset { z: number; } +export interface DrawingArgument { + argument: number; + value: number; +} + export interface UnitData { category: string; markerCategory: string; @@ -286,6 +291,8 @@ export interface UnitData { aimMethodRange: number; acquisitionRange: number; airborne: boolean; + cargoWeight: number; + drawingArguments: DrawingArgument[]; } export interface LoadoutItemBlueprint { diff --git a/frontend/react/src/map/latlng.ts b/frontend/react/src/map/latlng.ts new file mode 100644 index 00000000..87c5d949 --- /dev/null +++ b/frontend/react/src/map/latlng.ts @@ -0,0 +1,18 @@ +import * as L from "leaflet"; + +export class LatLng extends L.LatLng { + threshold: number; + + constructor(lat: number, lng: number, alt: number, threshold: number) { + super(lat, lng, alt); + this.threshold = threshold; + } + + setThreshold(threshold: number) { + this.threshold = threshold; + } + + getThreshold() { + return this.threshold; + } +} diff --git a/frontend/react/src/server/dataextractor.ts b/frontend/react/src/server/dataextractor.ts index 5eec77cb..9b2e87d6 100644 --- a/frontend/react/src/server/dataextractor.ts +++ b/frontend/react/src/server/dataextractor.ts @@ -1,5 +1,5 @@ import { LatLng } from "leaflet"; -import { Ammo, Contact, GeneralSettings, Offset, Radio, TACAN } from "../interfaces"; +import { Ammo, Contact, DrawingArgument, GeneralSettings, Offset, Radio, TACAN } from "../interfaces"; export class DataExtractor { #seekPosition = 0; @@ -58,7 +58,9 @@ export class DataExtractor { } extractLatLng() { - return new LatLng(this.extractFloat64(), this.extractFloat64(), this.extractFloat64()); + let latlng = new LatLng(this.extractFloat64(), this.extractFloat64(), this.extractFloat64()); + let threshold = this.extractFloat64(); + return latlng; } extractFromBitmask(bitmask: number, position: number) { @@ -104,6 +106,14 @@ export class DataExtractor { return value; } + extractDrawingArgument() { + const value: DrawingArgument = { + argument: this.extractUInt32(), + value: this.extractFloat64(), + }; + return value; + } + extractGeneralSettings() { const value: GeneralSettings = { prohibitJettison: this.extractBool(), @@ -159,4 +169,13 @@ export class DataExtractor { }; return value; } + + extractDrawingArguments() { + const value: DrawingArgument[] = []; + const size = this.extractUInt16(); + for (let idx = 0; idx < size; idx++) { + value.push(this.extractDrawingArgument()); + } + return value; + } } diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index ed07cd62..d8074cad 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -1,4 +1,4 @@ -import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point, LeafletMouseEvent, DomEvent, DomUtil, Circle } from "leaflet"; +import { LatLng, Polyline, DivIcon, CircleMarker, Map, Point, DomEvent } from "leaflet"; import { getApp } from "../olympusapp"; import { enumToCoalition, @@ -54,7 +54,7 @@ import { } from "../constants/constants"; import { DataExtractor } from "../server/dataextractor"; import { Weapon } from "../weapon/weapon"; -import { AlarmState, Ammo, Contact, GeneralSettings, LoadoutBlueprint, ObjectIconOptions, Offset, Radio, TACAN, UnitBlueprint, UnitData } from "../interfaces"; +import { AlarmState, Ammo, Contact, DrawingArgument, GeneralSettings, LoadoutBlueprint, ObjectIconOptions, Offset, Radio, TACAN, UnitBlueprint, UnitData } from "../interfaces"; import { RangeCircle } from "../map/rangecircle"; import { Group } from "./group"; import { ContextActionSet } from "./contextactionset"; @@ -159,6 +159,8 @@ export abstract class Unit extends CustomMarker { #racetrackAnchor: LatLng = new LatLng(0, 0); #racetrackBearing: number = 0; #airborne: boolean = false; + #cargoWeight: number = 0; + #drawingArguments: DrawingArgument[] = []; /* Other members used to draw the unit, mostly ancillary stuff like targets, ranges and so on */ #blueprint: UnitBlueprint | null = null; @@ -406,6 +408,12 @@ export abstract class Unit extends CustomMarker { getAirborne() { return this.#airborne; } + getCargoWeight() { + return this.#cargoWeight; + } + getDrawingArguments() { + return this.#drawingArguments; + } static getConstructor(type: string) { if (type === "GroundUnit") return GroundUnit; @@ -797,6 +805,12 @@ export abstract class Unit extends CustomMarker { case DataIndexes.airborne: this.#airborne = dataExtractor.extractBool(); break; + case DataIndexes.cargoWeight: + this.#cargoWeight = dataExtractor.extractFloat64(); + break; + case DataIndexes.drawingArguments: + this.#drawingArguments = dataExtractor.extractDrawingArguments(); + break; default: break; } @@ -920,6 +934,8 @@ export abstract class Unit extends CustomMarker { aimMethodRange: this.#aimMethodRange, acquisitionRange: this.#acquisitionRange, airborne: this.#airborne, + cargoWeight: this.#cargoWeight, + drawingArguments: this.#drawingArguments, }; } diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua index 4d3e8146..c098635c 100644 --- a/scripts/lua/backend/OlympusCommand.lua +++ b/scripts/lua/backend/OlympusCommand.lua @@ -23,6 +23,7 @@ Olympus.unitIndex = 0 -- Counter used to spread the computational load of data Olympus.unitStep = 50 -- Max number of units that get updated each cycle Olympus.units = {} -- Table holding references to all the currently existing units Olympus.unitsInitialLife = {} -- getLife0 returns 0 for ships, so we need to store the initial life of units +Olympus.drawArguments = {} -- Table that sets what drawArguments to read for each unit Olympus.weaponIndex = 0 -- Counter used to spread the computational load of data retrievial from DCS Olympus.weaponStep = 50 -- Max number of weapons that get updated each cycle @@ -1087,10 +1088,38 @@ function Olympus.setOnOff(groupName, onOff) end end +-- Get the unit description function getUnitDescription(unit) return unit:getDescr() end +-- Set the unit cargo weight +function Olympus.setCargoWeight(ID, weight) + Olympus.debug("Olympus.setCargoWeight " .. ID .. " " .. tostring(weight), 2) + + local unit = Olympus.getUnitByID(ID) + if unit ~= nil and unit:isExist() then + trigger.action.setUnitInternalCargo(unit:getName(), weight) + end +end + +-- Register a drawArgument to be read for a unit +function Olympus.registerDrawArgument(ID, argument, active) + Olympus.debug("Olympus.registerDrawArgument " .. ID .. " " .. tostring(argument) .. " " .. tostring(active), 2) + + -- Create the table if it does not exist + if Olympus.drawArguments[ID] == nil then + Olympus.drawArguments[ID] = {} + end + + -- Set the draw argument to true or false + if active then + Olympus.drawArguments[ID][argument] = true + else + Olympus.drawArguments[ID][argument] = false + end +end + -- This function gets the navpoints from the DCS mission function Olympus.getNavPoints() local function extract_tag(str) @@ -1293,6 +1322,20 @@ function Olympus.setUnitsData(arg, time) table["radarState"] = false end end ]] + + -- Read the draw arguments + local drawArguments = {} + if Olympus.drawArguments[ID] ~= nil then + for argument, active in pairs(Olympus.drawArguments[ID]) do + if active then + drawArguments[#drawArguments + 1] = { + argument = argument, + value = unit:getDrawArgumentValue(argument) + } + end + end + end + table["drawArguments"] = drawArguments local group = unit:getGroup() if group ~= nil then diff --git a/scripts/python/API/.vscode/launch.json b/scripts/python/API/.vscode/launch.json index 96695e79..9870d49a 100644 --- a/scripts/python/API/.vscode/launch.json +++ b/scripts/python/API/.vscode/launch.json @@ -5,18 +5,50 @@ "version": "0.2.0", "configurations": [ { - "name": "Voice control", + "name": "Python: Main", "type": "debugpy", "request": "launch", - "program": "voice_control.py", + "program": "${workspaceFolder}/main.py", "console": "integratedTerminal", "justMyCode": false, }, { - "name": "Test bed", + "name": "Example voice control", "type": "debugpy", "request": "launch", - "program": "testbed.py", + "program": "example_voice_control.py", + "console": "integratedTerminal", + "justMyCode": false, + }, + { + "name": "Example disembarked infantry", + "type": "debugpy", + "request": "launch", + "program": "example_disembarked_infantry.py", + "console": "integratedTerminal", + "justMyCode": false, + }, + { + "name": "Example set cargo weight", + "type": "debugpy", + "request": "launch", + "program": "example_set_cargo_weight.py", + "console": "integratedTerminal", + "justMyCode": false, + }, + { + "name": "Example draw argument", + "type": "debugpy", + "request": "launch", + "program": "example_draw_argument.py", + "console": "integratedTerminal", + "justMyCode": false, + }, + { + "name": "Example precise movement", + "type": "debugpy", + "request": "launch", + "program": "example_precise_movement.py", "console": "integratedTerminal", "justMyCode": false, } diff --git a/scripts/python/API/api.py b/scripts/python/API/api.py index 545fc23e..8f2e6adb 100644 --- a/scripts/python/API/api.py +++ b/scripts/python/API/api.py @@ -119,7 +119,122 @@ class API: signal.signal(signal.SIGINT, signal_handler) # Ctrl+C if hasattr(signal, 'SIGTERM'): signal.signal(signal.SIGTERM, signal_handler) # Termination signal + + async def _check_command_executed(self, command_hash: str, execution_callback, wait_for_result: bool, max_wait_time: int = 60): + """ + Check if a command has been executed by polling the API. + """ + start_time = time.time() + while True: + response = self._get(f"commands?commandHash={command_hash}") + if response.status_code == 200: + try: + data = response.json() + if data.get("commandExecuted") == True and (data.get("commandResult") is not None or (not wait_for_result)): + self.logger.info(f"Command {command_hash} executed successfully, command result: {data.get('commandResult')}") + if execution_callback: + await execution_callback(data.get("commandResult")) + break + elif data.get("status") == "failed": + self.logger.error(f"Command {command_hash} failed to execute.") + break + except ValueError: + self.logger.error("Failed to parse JSON response") + if time.time() - start_time > max_wait_time: + self.logger.warning(f"Timeout: Command {command_hash} did not complete within {max_wait_time} seconds.") + break + await asyncio.sleep(1) + async def _run_callback_async(self, callback, *args): + """ + Run a callback asynchronously, handling both sync and async callbacks. + """ + try: + if asyncio.iscoroutinefunction(callback): + await callback(*args) + else: + callback(*args) + except Exception as e: + # Log the error but don't crash the update process + self.logger.error(f"Error in callback: {e}") + + async def _run_async(self): + """ + Async implementation of the API service loop. + """ + # Setup signal handlers for graceful shutdown + self._setup_signal_handlers() + + # Here you can add any initialization logic if needed + self.logger.info("API started") + self.logger.info("Press Ctrl+C to stop gracefully") + + self.running = True + self.should_stop = False + + # Call the startup callback if registered + if self.on_startup_callback: + try: + await self._run_callback_async(self.on_startup_callback, self) + except Exception as e: + self.logger.error(f"Error in startup callback: {e}") + + try: + while not self.should_stop: + # Update units from the last update timestamp + self.update_units(self.units_update_timestamp) + + if self.on_update_callback: + await self._run_callback_async(self.on_update_callback, self) + await asyncio.sleep(self.interval) + except KeyboardInterrupt: + self.logger.info("Keyboard interrupt received") + self.stop() + finally: + self.logger.info("API stopped") + self.running = False + + def register_on_update_callback(self, callback): + """ + Register a callback function to be called on each update. + + Args: + callback (function): The function to call on update. Can be sync or async. + The function should accept a single argument, which is the API instance. + """ + self.on_update_callback = callback + + def unregister_on_update_callback(self): + """ + Unregister the callback function that is called on each update. + """ + self.on_update_callback = None + + def register_on_startup_callback(self, callback): + """ + Register a callback function to be called on startup. + Args: + callback (function): The function to call on startup. Can be sync or async. + The function should accept a single argument, which is the API instance. + """ + self.on_startup_callback = callback + + def unregister_on_startup_callback(self): + """ + Unregister the callback function that is called on startup. + """ + self.on_startup_callback = None + + def set_log_level(self, level): + """ + Set the logging level for the API. + + Args: + level: Logging level (e.g., logging.DEBUG, logging.INFO, logging.WARNING, self.logger.error) + """ + self.logger.setLevel(level) + self.logger.info(f"Log level set to {logging.getLevelName(level)}") + def get_units(self): """ Get all units from the API. Notice that if the API is not running, update_units() must be manually called first. @@ -170,8 +285,7 @@ class API: self.logger.error("Failed to parse JSON response") else: self.logger.error(f"Failed to fetch units: {response.status_code} - {response.text}") - - + def update_logs(self, time = 0): """ Fetch the logs from the API. @@ -192,7 +306,7 @@ class API: else: self.logger.error(f"Failed to fetch logs: {response.status_code} - {response.text}") - def spawn_aircrafts(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0): + def spawn_aircrafts(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0, execution_callback=None): """ Spawn aircraft units at the specified location or airbase. Args: @@ -202,6 +316,7 @@ class API: country (str): The country of the units. immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler. spawnPoints (int): Amount of spawn points to use, default is 0. + execution_callback (function): An optional async callback function to execute after the command is processed. """ command = { "units": [unit.toJSON() for unit in units], @@ -214,7 +329,21 @@ class API: data = { "spawnAircrafts": command } response = self._put(data) - def spawn_helicopters(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0): + # Parse the response as JSON if callback is provided + if execution_callback: + try: + response_data = response.json() + command_hash = response_data.get("commandHash", None) + if command_hash: + self.logger.info(f"Aircraft spawned successfully. Command Hash: {command_hash}") + # Start a background task to check if the command was executed + asyncio.create_task(self._check_command_executed(command_hash, execution_callback, wait_for_result=True)) + else: + self.logger.error("Command hash not found in response") + except ValueError: + self.logger.error("Failed to parse JSON response") + + def spawn_helicopters(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0, execution_callback=None): """ Spawn helicopter units at the specified location or airbase. Args: @@ -224,6 +353,7 @@ class API: country (str): The country of the units. immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler. spawnPoints (int): Amount of spawn points to use, default is 0. + execution_callback (function): An optional async callback function to execute after the command is processed. """ command = { "units": [unit.toJSON() for unit in units], @@ -236,6 +366,20 @@ class API: data = { "spawnHelicopters": command } response = self._put(data) + # Parse the response as JSON if callback is provided + if execution_callback: + try: + response_data = response.json() + command_hash = response_data.get("commandHash", None) + if command_hash: + self.logger.info(f"Helicopters spawned successfully. Command Hash: {command_hash}") + # Start a background task to check if the command was executed + asyncio.create_task(self._check_command_executed(command_hash, execution_callback, wait_for_result=True)) + else: + self.logger.error("Command hash not found in response") + except ValueError: + self.logger.error("Failed to parse JSON response") + def spawn_ground_units(self, units: list[UnitSpawnTable], coalition: str, country: str, immediate: bool, spawnPoints: int, execution_callback): """ Spawn ground units at the specified location. @@ -272,32 +416,7 @@ class API: except ValueError: self.logger.error("Failed to parse JSON response") - async def _check_command_executed(self, command_hash: str, execution_callback, wait_for_result: bool, max_wait_time: int = 60): - """ - Check if a command has been executed by polling the API. - """ - start_time = time.time() - while True: - response = self._get(f"commands?commandHash={command_hash}") - if response.status_code == 200: - try: - data = response.json() - if data.get("commandExecuted") == True and (data.get("commandResult") is not None or (not wait_for_result)): - self.logger.info(f"Command {command_hash} executed successfully, command result: {data.get('commandResult')}") - if execution_callback: - await execution_callback(data.get("commandResult")) - break - elif data.get("status") == "failed": - self.logger.error(f"Command {command_hash} failed to execute.") - break - except ValueError: - self.logger.error("Failed to parse JSON response") - if time.time() - start_time > max_wait_time: - self.logger.warning(f"Timeout: Command {command_hash} did not complete within {max_wait_time} seconds.") - break - await asyncio.sleep(1) - - def spawn_navy_units(self, units: list[UnitSpawnTable], coalition: str, country: str, immediate: bool, spawnPoints: int = 0): + def spawn_navy_units(self, units: list[UnitSpawnTable], coalition: str, country: str, immediate: bool, spawnPoints: int = 0, execution_callback=None): """ Spawn navy units at the specified location. Args: @@ -306,6 +425,7 @@ class API: country (str): The country of the units. immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler. spawnPoints (int): Amount of spawn points to use, default is 0. + execution_callback (function): An optional async callback function to execute after the command is processed. """ command = { "units": [unit.toJSON() for unit in units], @@ -316,6 +436,20 @@ class API: } data = { "spawnNavyUnits": command } response = self._put(data) + + # Parse the response as JSON if callback is provided + if execution_callback: + try: + response_data = response.json() + command_hash = response_data.get("commandHash", None) + if command_hash: + self.logger.info(f"Navy units spawned successfully. Command Hash: {command_hash}") + # Start a background task to check if the command was executed + asyncio.create_task(self._check_command_executed(command_hash, execution_callback, wait_for_result=True)) + else: + self.logger.error("Command hash not found in response") + except ValueError: + self.logger.error("Failed to parse JSON response") def create_radio_listener(self): """ @@ -327,55 +461,6 @@ class API: from radio.radio_listener import RadioListener return RadioListener(self, "localhost", self.config.get("audio").get("WSPort")) - def register_on_update_callback(self, callback): - """ - Register a callback function to be called on each update. - - Args: - callback (function): The function to call on update. Can be sync or async. - The function should accept a single argument, which is the API instance. - """ - self.on_update_callback = callback - - def register_on_startup_callback(self, callback): - """ - Register a callback function to be called on startup. - Args: - callback (function): The function to call on startup. Can be sync or async. - The function should accept a single argument, which is the API instance. - """ - self.on_startup_callback = callback - - def set_log_level(self, level): - """ - Set the logging level for the API. - - Args: - level: Logging level (e.g., logging.DEBUG, logging.INFO, logging.WARNING, self.logger.error) - """ - self.logger.setLevel(level) - self.logger.info(f"Log level set to {logging.getLevelName(level)}") - - def stop(self): - """ - Stop the API service gracefully. - """ - self.logger.info("Stopping API service...") - self.should_stop = True - - async def _run_callback_async(self, callback, *args): - """ - Run a callback asynchronously, handling both sync and async callbacks. - """ - try: - if asyncio.iscoroutinefunction(callback): - await callback(*args) - else: - callback(*args) - except Exception as e: - # Log the error but don't crash the update process - self.logger.error(f"Error in callback: {e}") - def generate_audio_message(text: str, gender: str = "male", code: str = "en-US") -> str: """ Generate a WAV file from text using Google Text-to-Speech API. @@ -412,28 +497,6 @@ class API: return file_name - def send_command(self, command: str): - """ - Send a command to the API. - - Args: - command (str): The command to send. - """ - response = self._put(command) - if response.status_code == 200: - self.logger.info(f"Command sent successfully: {command}") - else: - self.logger.error(f"Failed to send command: {response.status_code} - {response.text}") - - def run(self): - """ - Start the API service. - - This method initializes the API and starts the necessary components. - Sets up signal handlers for graceful shutdown. - """ - asyncio.run(self._run_async()) - def get_closest_units(self, coalitions: list[str], categories: list[str], position: LatLng, operate_as: str | None = None, max_number: int = 1, max_distance: float = 10000) -> list[Unit]: """ Get the closest units of a specific coalition and category to a given position. @@ -453,7 +516,7 @@ class API: # Iterate through all units and find the closest ones that match the criteria for unit in self.units.values(): - if unit.alive and unit.coalition in coalitions and unit.category.lower() in categories and (operate_as is None or unit.operate_as == operate_as or unit.coalition is not "neutral"): + if unit.alive and unit.coalition in coalitions and unit.category.lower() in categories and (operate_as is None or unit.operate_as == operate_as or unit.coalition != "neutral"): distance = position.distance_to(unit.position) if distance < closest_distance: closest_distance = distance @@ -468,39 +531,33 @@ class API: closest_units = closest_units[:max_number] return closest_units - - async def _run_async(self): - """ - Async implementation of the API service loop. - """ - # Setup signal handlers for graceful shutdown - self._setup_signal_handlers() - - # Here you can add any initialization logic if needed - self.logger.info("API started") - self.logger.info("Press Ctrl+C to stop gracefully") - - self.running = True - self.should_stop = False - - # Call the startup callback if registered - if self.on_startup_callback: - try: - await self._run_callback_async(self.on_startup_callback, self) - except Exception as e: - self.logger.error(f"Error in startup callback: {e}") - try: - while not self.should_stop: - # Update units from the last update timestamp - self.update_units(self.units_update_timestamp) - - if self.on_update_callback: - await self._run_callback_async(self.on_update_callback, self) - await asyncio.sleep(self.interval) - except KeyboardInterrupt: - self.logger.info("Keyboard interrupt received") - self.stop() - finally: - self.logger.info("API stopped") - self.running = False + def send_command(self, command: str): + """ + Send a command to the API. + + Args: + command (str): The command to send. + """ + response = self._put(command) + if response.status_code == 200: + self.logger.info(f"Command sent successfully: {command}") + else: + self.logger.error(f"Failed to send command: {response.status_code} - {response.text}") + + def stop(self): + """ + Stop the API service gracefully. + """ + self.logger.info("Stopping API service...") + self.should_stop = True + + def run(self): + """ + Start the API service. + + This method initializes the API and starts the necessary components. + Sets up signal handlers for graceful shutdown. + """ + asyncio.run(self._run_async()) + diff --git a/scripts/python/API/data/data_extractor.py b/scripts/python/API/data/data_extractor.py index de1608dd..bdcfebaf 100644 --- a/scripts/python/API/data/data_extractor.py +++ b/scripts/python/API/data/data_extractor.py @@ -1,6 +1,6 @@ import struct from typing import List -from data.data_types import LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset +from data.data_types import DrawArgument, LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset class DataExtractor: def __init__(self, buffer: bytes): @@ -48,6 +48,7 @@ class DataExtractor: lat = self.extract_float64() lng = self.extract_float64() alt = self.extract_float64() + threshold = self.extract_float64() return LatLng(lat, lng, alt) def extract_from_bitmask(self, bitmask: int, position: int) -> bool: @@ -136,4 +137,14 @@ class DataExtractor: x=self.extract_float64(), y=self.extract_float64(), z=self.extract_float64() - ) \ No newline at end of file + ) + + def extract_draw_arguments(self) -> List[DrawArgument]: + value = [] + size = self.extract_uint16() + for _ in range(size): + value.append(DrawArgument( + argument=self.extract_uint32(), + value=self.extract_float64() + )) + return value \ 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 38155fb2..54715d97 100644 --- a/scripts/python/API/data/data_indexes.py +++ b/scripts/python/API/data/data_indexes.py @@ -67,4 +67,6 @@ class DataIndexes(Enum): AIM_METHOD_RANGE = 63 ACQUISITION_RANGE = 64 AIRBORNE = 65 + CARGO_WEIGHT = 66 + DRAW_ARGUMENTS = 67 END_OF_DATA = 255 \ No newline at end of file diff --git a/scripts/python/API/data/data_types.py b/scripts/python/API/data/data_types.py index c4017a31..ffd73f27 100644 --- a/scripts/python/API/data/data_types.py +++ b/scripts/python/API/data/data_types.py @@ -8,13 +8,15 @@ class LatLng: lat: float lng: float alt: float - + threshold: Optional[float] = 0 # Optional threshold for proximity checks + def toJSON(self): """Convert LatLng to a JSON serializable dictionary.""" return { "lat": self.lat, "lng": self.lng, - "alt": self.alt + "alt": self.alt, + "threshold": self.threshold } def project_with_bearing_and_distance(self, d, bearing): @@ -88,4 +90,9 @@ class Contact: class Offset: x: float y: float - z: float \ No newline at end of file + z: float + +@dataclass +class DrawArgument: + argument: int + value: float \ No newline at end of file diff --git a/scripts/python/API/example_disembarked_infantry.py b/scripts/python/API/example_disembarked_infantry.py index 6ff1fd90..4f288bad 100644 --- a/scripts/python/API/example_disembarked_infantry.py +++ b/scripts/python/API/example_disembarked_infantry.py @@ -5,14 +5,14 @@ from math import pi # Setup a logger for the module import logging -logger = logging.getLogger("TestBed") +logger = logging.getLogger("example_disembarked_infantry") logger.setLevel(logging.INFO) handler = logging.StreamHandler() formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) -units_to_delete = None +units_to_delete = [] ############################################################################################# # This class represents a disembarked infantry unit that will engage in combat @@ -28,7 +28,7 @@ class DisembarkedInfantry(Unit): with the closest enemy unit. """ logger.info(f"Unit {self.unit_id} is now fighting.") - + # Pick a random target target = self.pick_random_target() @@ -86,7 +86,7 @@ class DisembarkedInfantry(Unit): # Simulate a firefight in the direction of the enemy firefight_destination = self.position.project_with_bearing_and_distance(30, bearing_to_enemy) - self.simulate_fire_fight(firefight_destination.lat, firefight_destination.lng, firefight_destination.alt + 1) + self.simulate_fire_fight(firefight_destination, firefight_destination.alt + 1) await asyncio.sleep(10) # Simulate some time spent in firefight self.start_fighting() # Restart the fighting process @@ -109,8 +109,8 @@ def on_api_startup(api: API): if unit.alive and not unit.human and unit.coalition == "blue": units_to_delete.append(unit) try: - unit.delete_unit(False, "", True) unit.register_on_property_change_callback("alive", on_unit_alive_change) + unit.delete_unit(False, "", True) logger.info(f"Deleted unit: {unit}") except Exception as e: @@ -175,7 +175,7 @@ def on_api_update(api: API): new_unit.__class__ = DisembarkedInfantry new_unit.start_fighting() - api.spawn_ground_units([spawn_table], unit.coalition, "", True, 0, lambda new_group_ID: execution_callback(new_group_ID)) + api.spawn_ground_units([spawn_table], unit.coalition, "", True, 0, execution_callback) logger.info(f"Spawned new unit succesfully at {spawn_position} with heading {unit.heading}") break diff --git a/scripts/python/API/example_draw_argument.py b/scripts/python/API/example_draw_argument.py new file mode 100644 index 00000000..b297c099 --- /dev/null +++ b/scripts/python/API/example_draw_argument.py @@ -0,0 +1,31 @@ +from api import API + +def on_api_startup(api: API): + units = api.update_units() + for unit in units.values(): + if unit.name == "UH-1H": + # Register draw argument 43 for UH-1H + unit.register_draw_argument(43) + +def on_api_update(api: API): + units = api.get_units() + for unit in units.values(): + if unit.name == "UH-1H": + print(f"Draw Arguments for {unit.name}:") + for draw_arg in unit.draw_arguments: + print(f" Argument: {draw_arg.argument}, Value: {draw_arg.value}") + +############################################################################################## +# Main entry point for the script. It registers the callbacks and starts the API. +############################################################################################## +if __name__ == "__main__": + # Initialize the API + api = API() + + # Register the callbacks + api.register_on_update_callback(on_api_update) + api.register_on_startup_callback(on_api_startup) + + # Start the API, this will run forever until stopped + api.run() + \ No newline at end of file diff --git a/scripts/python/API/example_precise_movement.py b/scripts/python/API/example_precise_movement.py new file mode 100644 index 00000000..fc4bf013 --- /dev/null +++ b/scripts/python/API/example_precise_movement.py @@ -0,0 +1,24 @@ +from api import API + +def on_api_startup(api: API): + units = api.update_units() + for unit in units.values(): + if unit.name == "Infantry AK Ins": + current_pos = unit.position + next_pos = current_pos.project_with_bearing_and_distance(20, 0) # Move 20 meters north + next_pos.threshold = 2 # Set threshold to 1 meter, very precise + unit.set_path([next_pos]) + +############################################################################################## +# Main entry point for the script. It registers the callbacks and starts the API. +############################################################################################## +if __name__ == "__main__": + # Initialize the API + api = API() + + # Register the callbacks + api.register_on_startup_callback(on_api_startup) + + # Start the API, this will run forever until stopped + api.run() + \ No newline at end of file diff --git a/scripts/python/API/example_set_cargo_weight.py b/scripts/python/API/example_set_cargo_weight.py new file mode 100644 index 00000000..c62cabe9 --- /dev/null +++ b/scripts/python/API/example_set_cargo_weight.py @@ -0,0 +1,29 @@ +from api import API + +def on_api_startup(api: API): + units = api.update_units() + for unit in units.values(): + if unit.name == "UH-1H": + # Set cargo weight to 5000 kg + unit.set_cargo_weight(5000.0) + +def on_api_update(api: API): + units = api.get_units() + for unit in units.values(): + if unit.name == "UH-1H": + print(f"Cargo Weight for {unit.name}: {unit.cargo_weight} kg") + +############################################################################################## +# Main entry point for the script. It registers the callbacks and starts the API. +############################################################################################## +if __name__ == "__main__": + # Initialize the API + api = API() + + # Register the callbacks + api.register_on_update_callback(on_api_update) + api.register_on_startup_callback(on_api_startup) + + # Start the API, this will run forever until stopped + api.run() + \ No newline at end of file diff --git a/scripts/python/API/voice_control.py b/scripts/python/API/example_voice_control.py similarity index 90% rename from scripts/python/API/voice_control.py rename to scripts/python/API/example_voice_control.py index bc1b1a43..0a073282 100644 --- a/scripts/python/API/voice_control.py +++ b/scripts/python/API/example_voice_control.py @@ -1,12 +1,13 @@ from math import pi +import os from api import API, UnitSpawnTable from radio.radio_listener import RadioListener # Setup a logger for the module import logging -logger = logging.getLogger("OlympusVoiceControl") +logger = logging.getLogger("example_voice_control") logger.setLevel(logging.INFO) handler = logging.StreamHandler() formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s') @@ -70,12 +71,15 @@ def on_message_received(recognized_text: str, unit_id: str, api: API, listener: message_filename = api.generate_audio_message("I did not understand") listener.transmit_on_frequency(message_filename, listener.frequency, listener.modulation, listener.encryption) + # Delete the message file after processing + os.remove(message_filename) + if __name__ == "__main__": api = API() logger.info("API initialized") listener = api.create_radio_listener() listener.start(frequency=251.000e6, modulation=0, encryption=0) - listener.register_message_callback(lambda wav_filename, unit_id, api=api, listener=listener: on_message_received(wav_filename, unit_id, api, listener)) + listener.register_message_callback(lambda recognized_text, unit_id, api=api, listener=listener: on_message_received(recognized_text, unit_id, api, listener)) api.run() \ No newline at end of file diff --git a/scripts/python/API/olympus.json b/scripts/python/API/olympus.json index 7159c6ee..b81d60e5 100644 --- a/scripts/python/API/olympus.json +++ b/scripts/python/API/olympus.json @@ -4,7 +4,7 @@ "port": 4512 }, "authentication": { - "gameMasterPassword": "a474219e5e9503c84d59500bb1bda3d9ade81e52d9fa1c234278770892a6dd74", + "gameMasterPassword": "a00a5973aacb17e4659125fbe10f4160d096dd84b2f586d2d75669462a30106d", "blueCommanderPassword": "7d2e1ef898b21db7411f725a945b76ec8dcad340ed705eaf801bc82be6fe8a4a", "redCommanderPassword": "abc5de7abdb8ed98f6d11d22c9d17593e339fde9cf4b9e170541b4f41af937e3" }, diff --git a/scripts/python/API/unit/temp_replace.py b/scripts/python/API/unit/temp_replace.py deleted file mode 100644 index 8fe34a54..00000000 --- a/scripts/python/API/unit/temp_replace.py +++ /dev/null @@ -1,18 +0,0 @@ -import re - -# Read the file -with open('unit.py', 'r', encoding='utf-8') as f: - content = f.read() - -# Pattern to match callback invocations -pattern = r'self\.on_property_change_callbacks\[\"(\w+)\"\]\(self, self\.(\w+)\)' -replacement = r'self._trigger_callback("\1", self.\2)' - -# Replace all matches -new_content = re.sub(pattern, replacement, content) - -# Write back to file -with open('unit.py', 'w', encoding='utf-8') as f: - f.write(new_content) - -print('Updated all callback invocations') diff --git a/scripts/python/API/unit/unit.py b/scripts/python/API/unit/unit.py index f64cdfa5..dc8e1d0f 100644 --- a/scripts/python/API/unit/unit.py +++ b/scripts/python/API/unit/unit.py @@ -3,7 +3,7 @@ import asyncio from data.data_extractor import DataExtractor from data.data_indexes import DataIndexes -from data.data_types import LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset +from data.data_types import DrawArgument, LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset from data.roes import ROES from data.states import states from utils.utils import enum_to_coalition @@ -81,6 +81,8 @@ class Unit: self.targeting_range = 0.0 self.aim_method_range = 0.0 self.acquisition_range = 0.0 + self.cargo_weight = 0.0 + self.draw_arguments: List[DrawArgument] = [] self.previous_total_ammo = 0 self.total_ammo = 0 @@ -654,6 +656,20 @@ class Unit: # Trigger callbacks for property change if "airborne" in self.on_property_change_callbacks: self._trigger_callback("airborne", self.airborne) + elif datum_index == DataIndexes.CARGO_WEIGHT.value: + cargo_weight = data_extractor.extract_float64() + if cargo_weight != self.cargo_weight: + self.cargo_weight = cargo_weight + # Trigger callbacks for property change + if "cargo_weight" in self.on_property_change_callbacks: + self._trigger_callback("cargo_weight", self.cargo_weight) + elif datum_index == DataIndexes.DRAW_ARGUMENTS.value: + draw_arguments = data_extractor.extract_draw_arguments() + if draw_arguments != self.draw_arguments: + self.draw_arguments = draw_arguments + # Trigger callbacks for property change + if "draw_arguments" in self.on_property_change_callbacks: + self._trigger_callback("draw_arguments", self.draw_arguments) # --- API functions requiring ID --- def set_path(self, path: List[LatLng]): @@ -758,6 +774,8 @@ class Unit: def set_engagement_properties(self, barrel_height, muzzle_velocity, aim_time, shots_to_fire, shots_base_interval, shots_base_scatter, engagement_range, targeting_range, aim_method_range, acquisition_range): return self.api.send_command({"setEngagementProperties": {"ID": self.ID, "barrelHeight": barrel_height, "muzzleVelocity": muzzle_velocity, "aimTime": aim_time, "shotsToFire": shots_to_fire, "shotsBaseInterval": shots_base_interval, "shotsBaseScatter": shots_base_scatter, "engagementRange": engagement_range, "targetingRange": targeting_range, "aimMethodRange": aim_method_range, "acquisitionRange": acquisition_range}}) - + def set_cargo_weight(self, cargo_weight: float): + return self.api.send_command({"setCargoWeight": {"ID": self.ID, "weight": cargo_weight}}) - \ No newline at end of file + 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