14 Commits

Author SHA1 Message Date
bobprofisker
a67540c12c Infantry boarding python script
First version of working python script to manage units embarking and disembarking more natively than DCS core can handle / do.
2025-10-10 20:04:15 +01:00
Pax1601
a257afca4b Add customString and customInteger to Unit data model
Introduced customString and customInteger fields to the Unit class in both backend (C++) and frontend (TypeScript/React). Updated data indexes, interfaces, and API handling to support setting and retrieving these custom fields. Also added UI elements in the unit control menu to display and handle these new properties.
2025-09-27 18:07:37 +02:00
Pax1601
3eef91fb24 Add cargo weight and draw argument support
Introduces cargo weight and draw argument properties to units across backend, frontend, and Python API. Adds related commands, data extraction, and registration logic, enabling setting and reading of cargo weight and custom draw arguments for units. Includes new API examples and updates to interfaces, data types, and Lua backend for full feature integration.
2025-09-11 21:47:11 +02:00
Pax1601
73a7ea74f3 feat: Added threshold to unit movement 2025-09-09 18:24:53 +02:00
Pax1601
4e6701ff01 Refactor API callbacks and improve example scripts
Moved register_on_update_callback in api.py for better code organization. Fixed initialization of units_to_delete and corrected simulate_fire_fight usage in example_disembarked_infantry.py. In example_voice_control.py, added cleanup of generated audio files and fixed callback parameter naming for clarity.
2025-08-08 13:14:59 +02:00
Pax1601
5fa1a26843 Add async callbacks and Kronos integration to API
Introduces async callback support for command execution in spawn methods, adds registration/unregistration for update and startup callbacks, and improves logging and signal handling. Adds a new Kronos module and main entry point for initializing and running the API with Kronos integration. Refactors example scripts and updates VSCode launch configurations for new entry points.
2025-08-08 11:06:53 +02:00
Pax1601
716b0dc48d Refactor unit command methods to use LatLng objects
Updated multiple methods in Unit to accept a LatLng object instead of separate lat/lng floats, improving type safety and consistency. Also made minor improvements and clarifications in the example_disembarked_infantry.py script, and added execution result handling in OlympusCommand.lua for spawned units.
2025-08-08 10:17:46 +02:00
Pax1601
c66c9242b3 Refactor Python API structure and enhance backend command handling
Major refactor of the Python API: moved modules into subdirectories, replaced app.py with api.py, and added new audio and utility modules. Backend C++ code now tracks command execution results, exposes them via the API, and improves command result handling. Also includes updates to the SRS audio handler, random string generation, and VSCode launch configurations.
2025-08-07 17:01:30 +02:00
Pax1601
4bcb5936b4 feat: Add initial API implementation and databases
Introduces a new Python API module with supporting scripts for data extraction, data types, unit management, and utility functions. Adds JSON databases for aircraft, helicopters, ground units, navy units, and mods, as well as configuration and VSCode launch settings. This provides the foundation for interacting with and managing units, spawning, and logging within the application.
2025-08-05 17:26:24 +02:00
Pax1601
4fd9b7e6c2 fix: Development build shown in release version 2025-08-05 11:05:29 +02:00
Pax1601
e4af9b06d3 fix: Ground units spawned in group are on top of each other, no heading when importing units
Units spawned via map, context menu, or unit spawn menu now include a heading property and are offset in latitude to prevent overlap. Also ensures heading is set during import/export. Minor JSX formatting improvements in UnitSpawnMenu.
2025-08-04 20:03:40 +02:00
Pax1601
89bd39cea8 Merge branch 'release-candidate' of https://github.com/Pax1601/DCSOlympus into release-candidate 2025-08-04 19:37:34 +02:00
Pax1601
cd34eebcba fix: ROE not set correctly when setting defaults
Changed the default ROE value in unit.h from OPEN_FIRE_WEAPON_FREE to OPEN_FIRE. Renamed mods.png to image.png in docs/images. Removed an unnecessary blank line in the audio section of olympus.json. Bumped version in version.json from v2.0.3 to v2.0.4.
2025-08-04 19:37:32 +02:00
Pax1601
07060112bc Merge pull request #1119 from Pax1601/A-4Skyhawk-mods-doc
Example mod files for A-4 Skyhawk
2025-08-02 18:36:44 +02:00
59 changed files with 69784 additions and 2401 deletions

View File

@@ -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();

View File

@@ -5,6 +5,11 @@
#include "logger.h"
#include "datatypes.h"
struct CommandResult {
string hash;
string result;
};
namespace CommandPriority {
enum CommandPriorities { LOW, MEDIUM, HIGH, IMMEDIATE };
};
@@ -533,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<void(void)> 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<void(void)> 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;
};

View File

@@ -70,6 +70,10 @@ namespace DataIndex {
aimMethodRange,
acquisitionRange,
airborne,
cargoWeight,
drawArguments,
customString,
customInteger,
lastIndex,
endOfData = 255
};
@@ -159,6 +163,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 +176,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;

View File

@@ -13,7 +13,14 @@ public:
void execute(lua_State* L);
void handleRequest(string key, json::value value, string username, json::value& answer);
bool checkSpawnPoints(int spawnPoints, string coalition);
bool isCommandExecuted(string commandHash) { return (find(executedCommandsHashes.begin(), executedCommandsHashes.end(), commandHash) != executedCommandsHashes.end()); }
bool isCommandExecuted(string commandHash) {
for (auto& commandResult : executedCommandResults) {
if (commandResult.hash == commandHash) {
return true;
}
}
return false;
}
void setFrameRate(double newFrameRate) { frameRate = newFrameRate; }
void setRestrictSpawns(bool newRestrictSpawns) { restrictSpawns = newRestrictSpawns; }
@@ -36,7 +43,7 @@ public:
private:
list<Command*> commands;
list<string> executedCommandsHashes;
list<CommandResult> executedCommandResults;
unsigned int load = 0;
double frameRate = 0;

View File

@@ -130,9 +130,13 @@ 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<DataTypes::DrawArgument> 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; };
virtual string getCategory() { return category; }
virtual bool getAlive() { return alive; }
virtual unsigned char getAlarmState() { return alarmState; }
virtual bool getHuman() { return human; }
@@ -197,6 +201,10 @@ public:
virtual double getAcquisitionRange() { return acquisitionRange; }
virtual bool getRadarState() { return radarState; }
virtual bool getAirborne() { return airborne; }
virtual double getCargoWeight() { return cargoWeight; }
virtual vector<DataTypes::DrawArgument> getDrawArguments() { return drawArguments; }
virtual string getCustomString() { return customString; }
virtual unsigned long getCustomInteger() { return customInteger; }
protected:
unsigned int ID;
@@ -240,7 +248,7 @@ protected:
Offset formationOffset = Offset(NULL);
unsigned int targetID = NULL;
Coords targetPosition = Coords(NULL);
unsigned char ROE = ROE::OPEN_FIRE_WEAPON_FREE;
unsigned char ROE = ROE::OPEN_FIRE;
unsigned char reactionToThreat = ReactionToThreat::EVADE_FIRE;
unsigned char emissionsCountermeasures = EmissionCountermeasure::DEFEND;
DataTypes::TACAN TACAN;
@@ -267,6 +275,11 @@ protected:
double aimMethodRange = 0;
double acquisitionRange = 0;
bool airborne = false;
double cargoWeight = 0;
vector<DataTypes::DrawArgument> drawArguments;
string customString = "";
unsigned long customInteger = 0;
/********** Other **********/
unsigned int taskCheckCounter = 0;

View File

@@ -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<Command*>(new SetCargoWeight(this->ID, cargoWeight));
scheduler->appendCommand(command);
}
}

View File

@@ -49,7 +49,6 @@ string SpawnGroundUnits::getString()
<< "heading = " << spawnOptions[i].heading << ", "
<< "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", "
<< "skill = \"" << spawnOptions[i].skill << "\"" << "}, ";
}
std::ostringstream commandSS;
@@ -59,6 +58,7 @@ string SpawnGroundUnits::getString()
<< "coalition = " << "\"" << coalition << "\"" << ", "
<< "country = \"" << country << "\", "
<< "units = " << "{" << unitsSS.str() << "}" << "}";
commandSS << ", \"" << this->getHash() << "\"";
return commandSS.str();
}
@@ -85,6 +85,7 @@ string SpawnNavyUnits::getString()
<< "coalition = " << "\"" << coalition << "\"" << ", "
<< "country = \"" << country << "\", "
<< "units = " << "{" << unitsSS.str() << "}" << "}";
commandSS << ", \"" << this->getHash() << "\"";
return commandSS.str();
}
@@ -113,6 +114,7 @@ string SpawnAircrafts::getString()
<< "airbaseName = \"" << airbaseName << "\", "
<< "country = \"" << country << "\", "
<< "units = " << "{" << unitsSS.str() << "}" << "}";
commandSS << ", \"" << this->getHash() << "\"";
return commandSS.str();
}
@@ -142,6 +144,7 @@ string SpawnHelicopters::getString()
<< "airbaseName = \"" << airbaseName << "\", "
<< "country = \"" << country << "\", "
<< "units = " << "{" << unitsSS.str() << "}" << "}";
commandSS << ", \"" << this->getHash() << "\"";
return commandSS.str();
}
@@ -315,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();
}

View File

@@ -23,6 +23,7 @@ Scheduler* scheduler = nullptr;
/* Data jsons */
json::value missionData = json::value::object();
json::value drawingsByLayer = json::value::object();
json::value executionResults = json::value::object();
mutex mutexLock;
string sessionHash;
@@ -174,5 +175,16 @@ extern "C" DllExport int coreDrawingsData(lua_State* L)
lua_getfield(L, -1, "drawingsByLayer");
luaTableToJSON(L, -1, drawingsByLayer);
return(0);
}
extern "C" DllExport int coreSetExecutionResults(lua_State* L)
{
/* Lock for thread safety */
lock_guard<mutex> guard(mutexLock);
lua_getglobal(L, "Olympus");
lua_getfield(L, -1, "executionResults");
luaTableToJSON(L, -1, executionResults, true);
return(0);
}

View File

@@ -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;
}

View File

@@ -52,10 +52,11 @@ void Scheduler::execute(lua_State* L)
if (command->getPriority() == priority)
{
string commandString = "Olympus.protectedCall(" + command->getString() + ")";
if (dostring_in(L, "server", (commandString)))
string resultString = "";
if (dostring_in(L, "server", (commandString), resultString))
log("Error executing command " + commandString);
else
log("Command '" + commandString + "' executed correctly, current load " + to_string(getLoad()));
log("Command '" + commandString + "' executed correctly, current load " + to_string(getLoad()) + ", result string: " + resultString);
/* Adjust the load depending on the fps */
double fpsMultiplier = 20;
@@ -64,7 +65,10 @@ void Scheduler::execute(lua_State* L)
load = static_cast<unsigned int>(command->getLoad() * fpsMultiplier);
commands.remove(command);
executedCommandsHashes.push_back(command->getHash());
CommandResult commandResult = {
command->getHash(), resultString
};
executedCommandResults.push_back(commandResult);
command->executeCallback(); /* Execute the command callback (this is a lambda function that can be used to execute a function when the command is run) */
delete command;
return;
@@ -164,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);
}
@@ -179,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<Command*>(new Smoke(color, loc));
log(username + " added a " + color + " smoke at (" + to_string(lat) + ", " + to_string(lng) + ")", true);
@@ -192,7 +202,6 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
string airbaseName = to_string(value[L"airbaseName"]);
string country = to_string(value[L"country"]);
int spawnPoints = value[L"spawnPoints"].as_number().to_int32();
if (!checkSpawnPoints(spawnPoints, coalition)) {
log(username + " insufficient spawn points ", true);
@@ -214,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)
@@ -248,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);
}
@@ -395,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);
@@ -424,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.");
}
}
@@ -553,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);
}
}
@@ -578,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);
}
}
/************************/
@@ -702,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);
}
/************************/
@@ -808,7 +818,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
command = dynamic_cast<Command*>(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);
@@ -818,6 +828,53 @@ 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<Command*>(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 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);

View File

@@ -18,6 +18,7 @@ extern WeaponsManager* weaponsManager;
extern Scheduler* scheduler;
extern json::value missionData;
extern json::value drawingsByLayer;
extern json::value executionResults;
extern mutex mutexLock;
extern string sessionHash;
extern string instancePath;
@@ -149,6 +150,10 @@ void Server::handle_get(http_request request)
}
else if (URI.compare(COMMANDS_URI) == 0 && query.find(L"commandHash") != query.end()) {
answer[L"commandExecuted"] = json::value(scheduler->isCommandExecuted(to_string(query[L"commandHash"])));
if (executionResults.has_field(query[L"commandHash"]))
answer[L"commandResult"] = executionResults[query[L"commandHash"]];
else
answer[L"commandResult"] = json::value::null();
}
/* Drawings data*/
else if (URI.compare(DRAWINGS_URI) == 0 && drawingsByLayer.has_object_field(L"drawings")) {

View File

@@ -128,6 +128,20 @@ void Unit::update(json::value json, double dt)
setAmmo(ammo);
}
if (json.has_object_field(L"drawArguments")) {
vector<DataTypes::DrawArgument> 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<DataTypes::Contact> contacts;
for (auto const& el : json[L"contacts"].as_object()) {
@@ -328,6 +342,10 @@ 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;
case DataIndex::customString: appendString(ss, datumIndex, customString); break;
case DataIndex::customInteger: appendNumeric(ss, datumIndex, customInteger); break;
}
}
}
@@ -699,6 +717,24 @@ void Unit::setGeneralSettings(DataTypes::GeneralSettings newGeneralSettings, boo
}
}
void Unit::setDrawArguments(vector<DataTypes::DrawArgument> 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) {
@@ -765,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)
@@ -774,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;

View File

@@ -7,6 +7,7 @@ void DllExport LogWarning(lua_State* L, string message);
void DllExport LogError(lua_State* L, string message);
void DllExport Log(lua_State* L, string message, unsigned int level);
int DllExport dostring_in(lua_State* L, string target, string command);
int DllExport dostring_in(lua_State* L, string target, string command, string& result);
void DllExport getAllUnits(lua_State* L, map<unsigned int, json::value>& unitJSONs);
unsigned int DllExport TACANChannelToFrequency(unsigned int channel, char XY);

View File

@@ -101,7 +101,24 @@ int dostring_in(lua_State* L, string target, string command)
lua_getfield(L, -1, "dostring_in");
lua_pushstring(L, target.c_str());
lua_pushstring(L, command.c_str());
return lua_pcall(L, 2, 0, 0);
int res = lua_pcall(L, 2, 0, 0);
return res;
}
int dostring_in(lua_State* L, string target, string command, string &result)
{
lua_getglobal(L, "net");
lua_getfield(L, -1, "dostring_in");
lua_pushstring(L, target.c_str());
lua_pushstring(L, command.c_str());
int res = lua_pcall(L, 2, 0, 0);
// Get the first result in the stack
if (lua_isstring(L, -1)) {
result = lua_tostring(L, -1);
}
return res;
}
unsigned int TACANChannelToFrequency(unsigned int channel, char XY)

View File

@@ -12,6 +12,7 @@ typedef int(__stdcall* f_coreUnitsData)(lua_State* L);
typedef int(__stdcall* f_coreWeaponsData)(lua_State* L);
typedef int(__stdcall* f_coreMissionData)(lua_State* L);
typedef int(__stdcall* f_coreDrawingsData)(lua_State* L);
typedef int(__stdcall* f_coreSetExecutionResults)(lua_State* L);
f_coreInit coreInit = nullptr;
f_coreDeinit coreDeinit = nullptr;
f_coreFrame coreFrame = nullptr;
@@ -19,6 +20,7 @@ f_coreUnitsData coreUnitsData = nullptr;
f_coreWeaponsData coreWeaponsData = nullptr;
f_coreMissionData coreMissionData = nullptr;
f_coreDrawingsData coreDrawingsData = nullptr;
f_coreSetExecutionResults coreExecutionResults = nullptr;
string modPath;
@@ -117,6 +119,13 @@ static int onSimulationStart(lua_State* L)
goto error;
}
coreExecutionResults = (f_coreSetExecutionResults)GetProcAddress(hGetProcIDDLL, "coreSetExecutionResults");
if (!coreExecutionResults)
{
LogError(L, "Error getting coreSetExecutionResults ProcAddress from DLL");
goto error;
}
coreInit(L, modPath.c_str());
LogInfo(L, "Module loaded and started successfully.");
@@ -213,6 +222,15 @@ static int setDrawingsData(lua_State* L)
return 0;
}
static int setExecutionResults(lua_State* L)
{
if (coreExecutionResults)
{
coreExecutionResults(L);
}
return 0;
}
static const luaL_Reg Map[] = {
{"onSimulationStart", onSimulationStart},
{"onSimulationFrame", onSimulationFrame},
@@ -221,6 +239,7 @@ static const luaL_Reg Map[] = {
{"setWeaponsData", setWeaponsData },
{"setMissionData", setMissionData },
{"setDrawingsData", setDrawingsData },
{"setExecutionResults", setExecutionResults },
{NULL, NULL}
};

View File

@@ -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 {

View File

@@ -1,5 +1,6 @@
#include "framework.h"
#include "utils.h"
#include <chrono>
// Get current date/time, format is YYYY-MM-DD.HH:mm:ss
const std::string CurrentDateTime()
@@ -44,7 +45,11 @@ std::string to_string(const std::wstring& wstr)
std::string random_string(size_t length)
{
srand(static_cast<unsigned int>(time(NULL)));
// Use nanoseconds since epoch as a seed for random number generation
auto now = std::chrono::high_resolution_clock::now();
auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
srand(static_cast<unsigned int>(nanos));
auto randchar = []() -> char
{
const char charset[] =
@@ -59,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; }

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -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",

View File

@@ -547,6 +547,10 @@ export enum DataIndexes {
aimMethodRange,
acquisitionRange,
airborne,
cargoWeight,
drawingArguments,
customString,
customInteger,
endOfData = 255,
}

View File

@@ -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,10 @@ export interface UnitData {
aimMethodRange: number;
acquisitionRange: number;
airborne: boolean;
cargoWeight: number;
drawingArguments: DrawingArgument[];
customString: string;
customInteger: number;
}
export interface LoadoutItemBlueprint {

View File

@@ -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;
}
}

View File

@@ -1055,7 +1055,13 @@ export class Map extends L.Map {
.getUnitsManager()
.spawnUnits(
this.#spawnRequestTable.category,
Array(this.#spawnRequestTable.amount).fill(this.#spawnRequestTable.unit),
Array(this.#spawnRequestTable.amount).fill(this.#spawnRequestTable.unit).map((unit, index) => {
return {
...unit,
location: new L.LatLng(unit.location.lat + (this.#spawnRequestTable?.category === "groundunit" ? 0.00025 * index : 0.005 * index), unit.location.lng),
heading: this.#spawnHeading,
};
}),
this.#spawnRequestTable.coalition,
false,
undefined,

View File

@@ -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;
}
}

View File

@@ -660,7 +660,13 @@ export function SpawnContextMenu(props: {}) {
.getUnitsManager()
.spawnUnits(
spawnRequestTable.category,
Array(spawnRequestTable.amount).fill(spawnRequestTable.unit),
Array(spawnRequestTable.amount).fill(spawnRequestTable.unit).map((unit, index) => {
return {
...unit,
location: new LatLng(unit.location.lat + (spawnRequestTable?.category === "groundunit" ? 0.00025 * index : 0.005 * index), unit.location.lng),
heading: unit.heading || 0,
};
}),
spawnRequestTable.coalition,
false
);

View File

@@ -319,7 +319,7 @@ export function ImportExportModal(props: { open: boolean }) {
if (selectionFilter[coalition][markerCategory] !== true) continue;
let unitsToSpawn = groupData.map((unitData: UnitData) => {
return { unitType: unitData.name, location: unitData.position, liveryID: "", skill: "High" };
return { unitType: unitData.name, location: unitData.position, liveryID: "", skill: "High", heading: unitData.heading || 0 };
});
getApp().getUnitsManager().spawnUnits(category.toLocaleLowerCase(), unitsToSpawn, coalition, false);

View File

@@ -109,7 +109,7 @@ export function Header() {
.then((res) => {
setLatestVersion(res["version"]);
if (VERSION === "{{OLYMPUS_VERSION_NUMBER}}") {
if (VERSION.includes("{OLYMPUS_VERSION_NUMBER}")) {
console.log("OLYMPUS_VERSION_NUMBER is not set. Skipping version check.");
setIsDevVersion(true);
} else {

File diff suppressed because it is too large Load Diff

View File

@@ -451,10 +451,9 @@ export function UnitSpawnMenu(props: {
`}
>
{props.blueprint?.liveries && props.blueprint?.liveries[id].countries.length == 1 && (
<img
src={`images/countries/${country?.flagCode.toLowerCase()}.svg`}
className={`h-6`}
/>
<img src={`images/countries/${country?.flagCode.toLowerCase()}.svg`} className={`
h-6
`} />
)}
<div className="my-auto truncate">
@@ -521,12 +520,9 @@ export function UnitSpawnMenu(props: {
<div className="my-auto flex flex-col gap-2">
<span>Spawn heading</span>
<div className="flex gap-1 text-sm text-gray-400">
<FaQuestionCircle className={`my-auto`} />{" "}
<div
className={`my-auto`}
>
Drag to change
</div>
<FaQuestionCircle className={`my-auto`} /> <div className={`
my-auto
`}>Drag to change</div>
</div>
</div>
@@ -632,7 +628,15 @@ export function UnitSpawnMenu(props: {
.getUnitsManager()
.spawnUnits(
spawnRequestTable.category,
Array(spawnRequestTable.amount).fill(spawnRequestTable.unit),
Array(spawnRequestTable.amount)
.fill(spawnRequestTable.unit)
.map((unit, index) => {
return {
...unit,
location: new LatLng(unit.location.lat + (spawnRequestTable?.category === "groundunit" ? 0.00025 * index : 0.005 * index), unit.location.lng),
heading: unit.heading || 0,
};
}),
spawnRequestTable.coalition,
false,
props.airbase?.getName() ?? undefined
@@ -912,9 +916,12 @@ export function UnitSpawnMenu(props: {
`}
>
{props.blueprint?.liveries && props.blueprint?.liveries[id].countries.length == 1 && (
<img src={`images/countries/${country?.flagCode.toLowerCase()}.svg`} className={`
<img
src={`images/countries/${country?.flagCode.toLowerCase()}.svg`}
className={`
h-6
`} />
`}
/>
)}
<div className="my-auto truncate">
@@ -980,12 +987,9 @@ export function UnitSpawnMenu(props: {
<div className="my-auto flex flex-col gap-2">
<span className="text-white">Spawn heading</span>
<div className="flex gap-1 text-sm text-gray-400">
<FaQuestionCircle className={`my-auto`} />{" "}
<div
className={`my-auto`}
>
Drag to change
</div>
<FaQuestionCircle className={`my-auto`} /> <div className={`
my-auto
`}>Drag to change</div>
</div>
</div>

View File

@@ -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,10 @@ export abstract class Unit extends CustomMarker {
#racetrackAnchor: LatLng = new LatLng(0, 0);
#racetrackBearing: number = 0;
#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;
@@ -406,6 +410,18 @@ export abstract class Unit extends CustomMarker {
getAirborne() {
return this.#airborne;
}
getCargoWeight() {
return this.#cargoWeight;
}
getDrawingArguments() {
return this.#drawingArguments;
}
getCustomString() {
return this.#customString;
}
getCustomInteger() {
return this.#customInteger;
}
static getConstructor(type: string) {
if (type === "GroundUnit") return GroundUnit;
@@ -797,6 +813,18 @@ 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;
case DataIndexes.customString:
this.#customString = dataExtractor.extractString();
break;
case DataIndexes.customInteger:
this.#customInteger = dataExtractor.extractUInt32();
break;
default:
break;
}
@@ -920,6 +948,10 @@ export abstract class Unit extends CustomMarker {
aimMethodRange: this.#aimMethodRange,
acquisitionRange: this.#acquisitionRange,
airborne: this.#airborne,
cargoWeight: this.#cargoWeight,
drawingArguments: this.#drawingArguments,
customString: this.#customString,
customInteger: this.#customInteger
};
}

View File

@@ -1,5 +1,6 @@
import { MessageType } from "./audiopacket";
import { defaultSRSData } from "./defaultdata";
import { AudioPacket } from "./audiopacket";
/* TCP/IP socket */
var net = require("net");
@@ -113,6 +114,16 @@ export class SRSHandler {
switch (data[0]) {
case MessageType.audio:
const encodedData = new Uint8Array(data.slice(1));
// Decoded the data for sanity check
if (encodedData.length < 22) {
console.log("Received audio data is too short, ignoring.");
return;
}
let packet = new AudioPacket();
packet.fromByteArray(encodedData);
this.udp.send(encodedData, this.SRSPort, "127.0.0.1", (error) => {
if (error) console.log(`Error sending data to SRS server: ${error}`);
});

View File

@@ -82,7 +82,6 @@
"_comment26": "The WSEndpoint is the endpoint used by the web interface to connect to the audio backend WebSocket when using a reverse proxy. A websocket proxy should be set up to forward requests from this endpoint to WSPort.",
"audio": {
"SRSPort": 5002,
"WSPort": 4000,
"WSEndpoint": "audio"

View File

@@ -14,6 +14,7 @@ Olympus.missionData = {}
Olympus.unitsData = {}
Olympus.weaponsData = {}
Olympus.drawingsByLayer = {}
Olympus.executionResults = {}
-- Units data structures
Olympus.unitCounter = 1 -- Counter to generate unique names
@@ -22,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
@@ -662,7 +664,7 @@ end
-- loadout: (string, optional) only for air units, must be one of the loadouts defined in unitPayloads.lua or mods.lua
-- payload: (table, optional) overrides loadout, specifies directly the loadout of the unit
-- liveryID: (string, optional)
function Olympus.spawnUnits(spawnTable)
function Olympus.spawnUnits(spawnTable, requestHash)
Olympus.debug("Olympus.spawnUnits " .. Olympus.serializeTable(spawnTable), 2)
local unitsTable = nil
@@ -710,10 +712,17 @@ function Olympus.spawnUnits(spawnTable)
task = 'CAP'
}
Olympus.debug(Olympus.serializeTable(vars), 2)
mist.dynAdd(vars)
local newGroup = mist.dynAdd(vars)
Olympus.unitCounter = Olympus.unitCounter + 1
Olympus.debug("Olympus.spawnUnits completed succesfully", 2)
if newGroup == nil then
Olympus.notify("Olympus.spawnUnits failed to spawn group: " .. Olympus.serializeTable(spawnTable), 30)
return nil
end
Olympus.executionResults[requestHash] = newGroup.groupId
end
-- Generates unit table for air units
@@ -1079,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)
@@ -1285,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
@@ -1498,6 +1549,11 @@ function Olympus.setWeaponsData(arg, time)
return time + 0.25
end
function Olympus.setExecutionResults()
Olympus.OlympusDLL.setExecutionResults()
return timer.getTime() + 1
end
function Olympus.setMissionData(arg, time)
-- Bullseye data
local bullseyes = {}
@@ -1697,6 +1753,7 @@ world.addEventHandler(handler)
timer.scheduleFunction(Olympus.setUnitsData, {}, timer.getTime() + 0.05)
timer.scheduleFunction(Olympus.setWeaponsData, {}, timer.getTime() + 0.25)
timer.scheduleFunction(Olympus.setMissionData, {}, timer.getTime() + 1)
timer.scheduleFunction(Olympus.setExecutionResults, {}, timer.getTime() + 1)
-- Initialize the ME units
Olympus.initializeUnits()

64
scripts/python/API/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,64 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Main",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"console": "integratedTerminal",
"justMyCode": false,
},
{
"name": "Example voice control",
"type": "debugpy",
"request": "launch",
"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,
},
{
"name": "Infantry boarding",
"type": "debugpy",
"request": "launch",
"program": "infantry_boarding.py",
"console": "integratedTerminal",
"justMyCode": false,
}
]
}

563
scripts/python/API/api.py Normal file
View File

@@ -0,0 +1,563 @@
import json
import time
import requests
import base64
import signal
import sys
import logging
import os
import tempfile
import asyncio
from google.cloud import speech, texttospeech
# Custom imports
from data.data_extractor import DataExtractor
from unit.unit import Unit
from data.unit_spawn_table import UnitSpawnTable
from data.data_types import LatLng
class API:
def __init__(self, username: str = "API", databases_location: str = "databases"):
self.base_url = None
self.config = None
self.logs = {}
self.units: dict[str, Unit] = {}
self.username = username
self.databases_location = databases_location
self.interval = 1 # Default update interval in seconds
self.on_update_callback = None
self.on_startup_callback = None
self.should_stop = False
self.running = False
self.units_update_timestamp = 0
# Setup logging
self.logger = logging.getLogger(f"DCSOlympus.API")
if not self.logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO)
# Read the config file olympus.json
try:
with open("olympus.json", "r") as file:
# Load the JSON configuration
self.config = json.load(file)
except FileNotFoundError:
self.logger.error("Configuration file olympus.json not found.")
self.password = self.config.get("authentication").get("gameMasterPassword")
address = self.config.get("backend").get("address")
port = self.config.get("backend").get("port", None)
if port:
self.base_url = f"http://{address}:{port}/olympus"
else:
self.base_url = f"https://{address}/olympus"
# Read the aircraft, helicopter, groundunit and navyunit databases as json files
try:
with open(f"{self.databases_location}/aircraftdatabase.json", "r", -1, 'utf-8') as file:
self.aircraft_database = json.load(file)
except FileNotFoundError:
self.logger.error("Aircraft database file not found.")
try:
with open(f"{self.databases_location}/helicopterdatabase.json", "r", -1, 'utf-8') as file:
self.helicopter_database = json.load(file)
except FileNotFoundError:
self.logger.error("Helicopter database file not found.")
try:
with open(f"{self.databases_location}/groundunitdatabase.json", "r", -1, 'utf-8') as file:
self.groundunit_database = json.load(file)
except FileNotFoundError:
self.logger.error("Ground unit database file not found.")
try:
with open(f"{self.databases_location}/navyunitdatabase.json", "r", -1, 'utf-8') as file:
self.navyunit_database = json.load(file)
except FileNotFoundError:
self.logger.error("Navy unit database file not found.")
def _get(self, endpoint):
credentials = f"{self.username}:{self.password}"
base64_encoded_credentials = base64.b64encode(credentials.encode()).decode()
headers = {
"Authorization": f"Basic {base64_encoded_credentials}"
}
response = requests.get(f"{self.base_url}/{endpoint}", headers=headers)
if response.status_code == 200:
return response
else:
response.raise_for_status()
def _put(self, data):
credentials = f"{self.username}:{self.password}"
base64_encoded_credentials = base64.b64encode(credentials.encode()).decode()
headers = {
"Authorization": f"Basic {base64_encoded_credentials}",
"Content-Type": "application/json"
}
response = requests.put(f"{self.base_url}", headers=headers, json=data)
if response.status_code == 200:
return response
else:
response.raise_for_status()
def _setup_signal_handlers(self):
def signal_handler(signum, frame):
self.logger.info(f"Received signal {signum}, initiating graceful shutdown...")
self.stop()
# Register signal handlers
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.
Returns:
dict: A dictionary of Unit objects indexed by their unit ID.
"""
return self.units
def get_logs(self):
"""
Get the logs from the API. Notice that if the API is not running, update_logs() must be manually called first.
Returns:
dict: A dictionary of log entries indexed by their log ID.
"""
return self.logs
def update_units(self, time=0):
"""
Fetch the list of units from the API.
Args:
time (int): The time in milliseconds from Unix epoch to fetch units from. Default is 0, which fetches all units.
If time is greater than 0, it fetches units updated after that time.
Returns:
dict: A dictionary of Unit objects indexed by their unit ID.
"""
response = self._get("units")
if response.status_code == 200 and len(response.content) > 0:
try:
data_extractor = DataExtractor(response.content)
# Extract the update timestamp
self.units_update_timestamp = data_extractor.extract_uint64()
self.logger.debug(f"Update Timestamp: {self.units_update_timestamp}")
while data_extractor.get_seek_position() < len(response.content):
# Extract the unit ID
unit_id = data_extractor.extract_uint32()
if unit_id not in self.units:
# Create a new Unit instance if it doesn't exist
self.units[unit_id] = Unit(unit_id, self)
self.units[unit_id].update_from_data_extractor(data_extractor)
return self.units
except ValueError:
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.
Args:
time (int): The time in milliseconds from Unix epoch to fetch logs from. Default is 0, which fetches all logs.
Returns:
list: A list of log entries.
"""
endpoint = "/logs"
endpoint += f"?time={time}"
response = self._get(endpoint)
if response.status_code == 200:
try:
self.logs = json.loads(response.content.decode('utf-8'))
return self.logs
except ValueError:
self.logger.error("Failed to parse JSON response")
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, execution_callback=None):
"""
Spawn aircraft units at the specified location or airbase.
Args:
units (list[UnitSpawnTable]): List of UnitSpawnTable objects representing the aircraft to spawn.
coalition (str): The coalition to which the units belong. ("blue", "red", "neutral")
airbaseName (str): The name of the airbase where the units will be spawned. Leave "" for air spawn.
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],
"coalition": coalition,
"airbaseName": airbaseName,
"country": country,
"immediate": immediate,
"spawnPoints": spawnPoints,
}
data = { "spawnAircrafts": 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"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:
units (list[UnitSpawnTable]): List of UnitSpawnTable objects representing the helicopters to spawn.
coalition (str): The coalition to which the units belong. ("blue", "red", "neutral")
airbaseName (str): The name of the airbase where the units will be spawned. Leave "" for air spawn.
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],
"coalition": coalition,
"airbaseName": airbaseName,
"country": country,
"immediate": immediate,
"spawnPoints": spawnPoints,
}
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.
Args:
units (list[UnitSpawnTable]): List of UnitSpawnTable objects representing the ground units to spawn.
coalition (str): The coalition to which the units belong. ("blue", "red", "neutral")
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.
execution_callback (function): An async callback function to execute after the command is processed.
"""
command = {
"units": [unit.toJSON() for unit in units],
"coalition": coalition,
"country": country,
"immediate": immediate,
"spawnPoints": spawnPoints,
}
data = { "spawnGroundUnits": command }
response = self._put(data)
# Parse the response as JSON
try:
response_data = response.json()
command_hash = response_data.get("commandHash", None)
if command_hash:
self.logger.info(f"Ground 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 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:
units (list[UnitSpawnTable]): List of UnitSpawnTable objects representing the navy units to spawn.
coalition (str): The coalition to which the units belong. ("blue", "red", "neutral")
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],
"coalition": coalition,
"country": country,
"immediate": immediate,
"spawnPoints": spawnPoints,
}
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):
"""
Create an audio listener instance.
Returns:
AudioListener: An instance of the AudioListener class.
"""
from radio.radio_listener import RadioListener
return RadioListener(self, "localhost", self.config.get("audio").get("WSPort"))
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.
Remember to manually delete the generated file after use!
Args:
text (str): The text to synthesize.
gender (str): The gender of the voice (male or female).
code (str): The language code (e.g., en-US).
Returns:
str: The filename of the generated WAV file.
"""
client = texttospeech.TextToSpeechClient()
input_text = texttospeech.SynthesisInput(text=text)
voice = texttospeech.VoiceSelectionParams(
language_code=code,
ssml_gender=texttospeech.SsmlVoiceGender.MALE if gender == "male" else texttospeech.SsmlVoiceGender.FEMALE
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.LINEAR16,
sample_rate_hertz=16000
)
response = client.synthesize_speech(
input=input_text,
voice=voice,
audio_config=audio_config
)
# Save the response audio to a WAV file
temp_dir = tempfile.gettempdir()
file_name = os.path.join(temp_dir, next(tempfile._get_candidate_names()) + ".wav")
with open(file_name, "wb") as out:
out.write(response.audio_content)
return file_name
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.
Units are filtered by coalition, category, and optionally by operating role.
Args:
coalitions (list[str]): List of coalitions to filter by (e.g., ["blue", "red"]).
categories (list[str]): List of categories to filter by (e.g., ["aircraft", "groundunit"]).
position (LatLng): The position to measure distance from.
operate_as (str | None): Optional list of operating roles to filter by (either "red" or "blue"). Default is None.
max_number (int): Maximum number of closest units to return. Default is 1.
max_distance (float): Maximum distance to consider for the closest unit. Default is 10000 meters.
"""
closest_units = []
closest_distance = max_distance
# 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 != "neutral"):
distance = position.distance_to(unit.position)
if distance < closest_distance:
closest_distance = distance
closest_units = [unit]
elif distance == closest_distance:
closest_units.append(unit)
# Sort the closest units by distance
closest_units.sort(key=lambda u: position.distance_to(u.position))
# Limit the number of closest units returned
closest_units = closest_units[:max_number]
return closest_units
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())

View File

@@ -0,0 +1,205 @@
from enum import Enum
from typing import List, Dict, Optional
import struct
packet_id = 0
class MessageType(Enum):
AUDIO = 0
SETTINGS = 1
CLIENTS_DATA = 2
class AudioPacket:
def __init__(self):
# Mandatory data
self._frequencies: List[Dict[str, int]] = []
self._audio_data: Optional[bytes] = None
self._transmission_guid: Optional[str] = None
self._client_guid: Optional[str] = None
# Default data
self._unit_id: int = 0
self._hops: int = 0
# Usually internally set only
self._packet_id: Optional[int] = None
def from_byte_array(self, byte_array: bytes):
total_length = self._byte_array_to_integer(byte_array[0:2])
audio_length = self._byte_array_to_integer(byte_array[2:4])
frequencies_length = self._byte_array_to_integer(byte_array[4:6])
# Perform some sanity checks
if total_length != len(byte_array):
print(f"Warning, audio packet expected length is {total_length} but received length is {len(byte_array)}, aborting...")
return
if frequencies_length % 10 != 0:
print(f"Warning, audio packet frequencies data length is {frequencies_length} which is not a multiple of 10, aborting...")
return
# Extract the audio data
self._audio_data = byte_array[6:6 + audio_length]
# Extract the frequencies
offset = 6 + audio_length
for idx in range(frequencies_length // 10):
self._frequencies.append({
'frequency': self._byte_array_to_double(byte_array[offset:offset + 8]),
'modulation': byte_array[offset + 8],
'encryption': byte_array[offset + 9]
})
offset += 10
# Extract the remaining data
self._unit_id = self._byte_array_to_integer(byte_array[offset:offset + 4])
offset += 4
self._packet_id = self._byte_array_to_integer(byte_array[offset:offset + 8])
offset += 8
self._hops = self._byte_array_to_integer(byte_array[offset:offset + 1])
offset += 1
self._transmission_guid = byte_array[offset:offset + 22].decode('utf-8', errors='ignore')
offset += 22
self._client_guid = byte_array[offset:offset + 22].decode('utf-8', errors='ignore')
offset += 22
def to_byte_array(self) -> Optional[bytes]:
global packet_id
# Perform some sanity checks
if len(self._frequencies) == 0:
print("Warning, could not encode audio packet, no frequencies data provided, aborting...")
return None
if self._audio_data is None:
print("Warning, could not encode audio packet, no audio data provided, aborting...")
return None
if self._transmission_guid is None:
print("Warning, could not encode audio packet, no transmission GUID provided, aborting...")
return None
if self._client_guid is None:
print("Warning, could not encode audio packet, no client GUID provided, aborting...")
return None
# Prepare the array for the header
header = [0, 0, 0, 0, 0, 0]
# Encode the frequencies data
frequencies_data = []
for data in self._frequencies:
frequencies_data.extend(self._double_to_byte_array(data['frequency']))
frequencies_data.append(data['modulation'])
frequencies_data.append(data['encryption'])
# If necessary increase the packet_id
if self._packet_id is None:
self._packet_id = packet_id
packet_id += 1
# Encode unitID, packetID, hops
enc_unit_id = self._integer_to_byte_array(self._unit_id, 4)
enc_packet_id = self._integer_to_byte_array(self._packet_id, 8)
enc_hops = [self._hops]
# Assemble packet
encoded_data = []
encoded_data.extend(header)
encoded_data.extend(list(self._audio_data))
encoded_data.extend(frequencies_data)
encoded_data.extend(enc_unit_id)
encoded_data.extend(enc_packet_id)
encoded_data.extend(enc_hops)
encoded_data.extend(list(self._transmission_guid.encode('utf-8')))
encoded_data.extend(list(self._client_guid.encode('utf-8')))
# Set the lengths of the parts
enc_packet_len = self._integer_to_byte_array(len(encoded_data), 2)
encoded_data[0] = enc_packet_len[0]
encoded_data[1] = enc_packet_len[1]
enc_audio_len = self._integer_to_byte_array(len(self._audio_data), 2)
encoded_data[2] = enc_audio_len[0]
encoded_data[3] = enc_audio_len[1]
frequency_audio_len = self._integer_to_byte_array(len(frequencies_data), 2)
encoded_data[4] = frequency_audio_len[0]
encoded_data[5] = frequency_audio_len[1]
return bytes([0] + encoded_data)
# Utility methods for byte array conversion
def _byte_array_to_integer(self, byte_array: bytes) -> int:
if len(byte_array) == 1:
return struct.unpack('<B', byte_array)[0]
elif len(byte_array) == 2:
return struct.unpack('<H', byte_array)[0]
elif len(byte_array) == 4:
return struct.unpack('<I', byte_array)[0]
elif len(byte_array) == 8:
return struct.unpack('<Q', byte_array)[0]
else:
raise ValueError(f"Unsupported byte array length: {len(byte_array)}")
def _byte_array_to_double(self, byte_array: bytes) -> float:
return struct.unpack('<d', byte_array)[0]
def _integer_to_byte_array(self, value: int, length: int) -> List[int]:
if length == 1:
return list(struct.pack('<B', value))
elif length == 2:
return list(struct.pack('<H', value))
elif length == 4:
return list(struct.pack('<I', value))
elif length == 8:
return list(struct.pack('<Q', value))
else:
raise ValueError(f"Unsupported length: {length}")
def _double_to_byte_array(self, value: float) -> List[int]:
return list(struct.pack('<d', value))
# Getters and Setters
def set_frequencies(self, frequencies: List[Dict[str, int]]):
self._frequencies = frequencies
def get_frequencies(self) -> List[Dict[str, int]]:
return self._frequencies
def set_audio_data(self, audio_data: bytes):
self._audio_data = audio_data
def get_audio_data(self) -> Optional[bytes]:
return self._audio_data
def set_transmission_guid(self, transmission_guid: str):
self._transmission_guid = transmission_guid
def get_transmission_guid(self) -> Optional[str]:
return self._transmission_guid
def set_client_guid(self, client_guid: str):
self._client_guid = client_guid
def get_client_guid(self) -> Optional[str]:
return self._client_guid
def set_unit_id(self, unit_id: int):
self._unit_id = unit_id
def get_unit_id(self) -> int:
return self._unit_id
def set_packet_id(self, packet_id: int):
self._packet_id = packet_id
def get_packet_id(self) -> Optional[int]:
return self._packet_id
def set_hops(self, hops: int):
self._hops = hops
def get_hops(self) -> int:
return self._hops

View File

@@ -0,0 +1,75 @@
import threading
import opuslib # TODO: important, setup dll recognition
import wave
from typing import Callable
from audio.audio_packet import AudioPacket
import tempfile
import os
class AudioRecorder:
def __init__(self, api):
self.packets: list[AudioPacket] = []
self.silence_timer = None
self.recording_callback = None
self.api = api
def register_recording_callback(self, callback: Callable[[AudioPacket], None]):
"""Set the callback function for handling recorded audio packets."""
self.recording_callback = callback
def add_packet(self, packet: AudioPacket):
self.packets.append(packet)
# Start a countdown timer to stop recording after 2 seconds of silence
self.start_silence_timer()
def stop_recording(self):
if self.silence_timer:
self.silence_timer.cancel()
self.silence_timer = None
# Extract the client GUID from the first packet if available
unit_ID = self.packets[0].get_unit_id() if self.packets else None
# Process the recorded packets
if self.packets:
print(f"Stopping recording, total packets: {len(self.packets)}")
# Reorder the packets according to their packet ID
self.packets.sort(key=lambda p: p.get_packet_id())
# Decode to audio data using the opus codec
opus_decoder = opuslib.Decoder(16000, 1)
audio_data = bytearray()
for packet in self.packets:
decoded_data = opus_decoder.decode(packet.get_audio_data(), frame_size=6400)
audio_data.extend(decoded_data)
# Save the audio into a temporary wav file with a random name in the tempo folder
temp_dir = tempfile.gettempdir()
file_name = os.path.join(temp_dir, next(tempfile._get_candidate_names()) + ".wav")
with wave.open(file_name, "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(16000)
wav_file.writeframes(audio_data)
if self.recording_callback:
self.recording_callback(file_name, unit_ID)
# Clear the packets after saving and delete the temporary file
os.remove(file_name)
self.packets.clear()
else:
print("No packets recorded.")
def start_silence_timer(self):
if self.silence_timer:
self.silence_timer.cancel()
# Set a timer for 2 seconds
self.silence_timer = threading.Timer(2.0, self.stop_recording)
self.silence_timer.start()

View File

@@ -0,0 +1,150 @@
import struct
from typing import List
from data.data_types import DrawArgument, LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset
class DataExtractor:
def __init__(self, buffer: bytes):
self._seek_position = 0
self._buffer = buffer
self._length = len(buffer)
def set_seek_position(self, seek_position: int):
self._seek_position = seek_position
def get_seek_position(self) -> int:
return self._seek_position
def extract_bool(self) -> bool:
value = struct.unpack_from('<B', self._buffer, self._seek_position)[0]
self._seek_position += 1
return value > 0
def extract_uint8(self) -> int:
value = struct.unpack_from('<B', self._buffer, self._seek_position)[0]
self._seek_position += 1
return value
def extract_uint16(self) -> int:
value = struct.unpack_from('<H', self._buffer, self._seek_position)[0]
self._seek_position += 2
return value
def extract_uint32(self) -> int:
value = struct.unpack_from('<I', self._buffer, self._seek_position)[0]
self._seek_position += 4
return value
def extract_uint64(self) -> int:
value = struct.unpack_from('<Q', self._buffer, self._seek_position)[0]
self._seek_position += 8
return value
def extract_float64(self) -> float:
value = struct.unpack_from('<d', self._buffer, self._seek_position)[0]
self._seek_position += 8
return value
def extract_lat_lng(self) -> LatLng:
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:
return ((bitmask >> position) & 1) > 0
def extract_string(self, length: int = None) -> str:
if length is None:
length = self.extract_uint16()
string_buffer = self._buffer[self._seek_position:self._seek_position + length]
# Find null terminator
string_length = length
for idx, byte_val in enumerate(string_buffer):
if byte_val == 0:
string_length = idx
break
try:
value = string_buffer[:string_length].decode('utf-8').strip()
except UnicodeDecodeError:
value = string_buffer[:string_length].decode('utf-8', errors='ignore').strip()
self._seek_position += length
return value
def extract_char(self) -> str:
return self.extract_string(1)
def extract_tacan(self) -> TACAN:
return TACAN(
is_on=self.extract_bool(),
channel=self.extract_uint8(),
xy=self.extract_char(),
callsign=self.extract_string(4)
)
def extract_radio(self) -> Radio:
return Radio(
frequency=self.extract_uint32(),
callsign=self.extract_uint8(),
callsign_number=self.extract_uint8()
)
def extract_general_settings(self) -> GeneralSettings:
return GeneralSettings(
prohibit_jettison=self.extract_bool(),
prohibit_aa=self.extract_bool(),
prohibit_ag=self.extract_bool(),
prohibit_afterburner=self.extract_bool(),
prohibit_air_wpn=self.extract_bool()
)
def extract_ammo(self) -> List[Ammo]:
value = []
size = self.extract_uint16()
for _ in range(size):
value.append(Ammo(
quantity=self.extract_uint16(),
name=self.extract_string(33),
guidance=self.extract_uint8(),
category=self.extract_uint8(),
missile_category=self.extract_uint8()
))
return value
def extract_contacts(self) -> List[Contact]:
value = []
size = self.extract_uint16()
for _ in range(size):
value.append(Contact(
id=self.extract_uint32(),
detection_method=self.extract_uint8()
))
return value
def extract_active_path(self) -> List[LatLng]:
value = []
size = self.extract_uint16()
for _ in range(size):
value.append(self.extract_lat_lng())
return value
def extract_offset(self) -> Offset:
return Offset(
x=self.extract_float64(),
y=self.extract_float64(),
z=self.extract_float64()
)
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

View File

@@ -0,0 +1,74 @@
from enum import Enum
class DataIndexes(Enum):
START_OF_DATA = 0
CATEGORY = 1
ALIVE = 2
ALARM_STATE = 3
RADAR_STATE = 4
HUMAN = 5
CONTROLLED = 6
COALITION = 7
COUNTRY = 8
NAME = 9
UNIT_NAME = 10
CALLSIGN = 11
UNIT_ID = 12
GROUP_ID = 13
GROUP_NAME = 14
STATE = 15
TASK = 16
HAS_TASK = 17
POSITION = 18
SPEED = 19
HORIZONTAL_VELOCITY = 20
VERTICAL_VELOCITY = 21
HEADING = 22
TRACK = 23
IS_ACTIVE_TANKER = 24
IS_ACTIVE_AWACS = 25
ON_OFF = 26
FOLLOW_ROADS = 27
FUEL = 28
DESIRED_SPEED = 29
DESIRED_SPEED_TYPE = 30
DESIRED_ALTITUDE = 31
DESIRED_ALTITUDE_TYPE = 32
LEADER_ID = 33
FORMATION_OFFSET = 34
TARGET_ID = 35
TARGET_POSITION = 36
ROE = 37
REACTION_TO_THREAT = 38
EMISSIONS_COUNTERMEASURES = 39
TACAN = 40
RADIO = 41
GENERAL_SETTINGS = 42
AMMO = 43
CONTACTS = 44
ACTIVE_PATH = 45
IS_LEADER = 46
OPERATE_AS = 47
SHOTS_SCATTER = 48
SHOTS_INTENSITY = 49
HEALTH = 50
RACETRACK_LENGTH = 51
RACETRACK_ANCHOR = 52
RACETRACK_BEARING = 53
TIME_TO_NEXT_TASKING = 54
BARREL_HEIGHT = 55
MUZZLE_VELOCITY = 56
AIM_TIME = 57
SHOTS_TO_FIRE = 58
SHOTS_BASE_INTERVAL = 59
SHOTS_BASE_SCATTER = 60
ENGAGEMENT_RANGE = 61
TARGETING_RANGE = 62
AIM_METHOD_RANGE = 63
ACQUISITION_RANGE = 64
AIRBORNE = 65
CARGO_WEIGHT = 66
DRAW_ARGUMENTS = 67
CUSTOM_STRING = 68
CUSTOM_INTEGER = 69
END_OF_DATA = 255

View File

@@ -0,0 +1,98 @@
from dataclasses import dataclass
from typing import List, Optional
from utils.utils import bearing_to, distance, project_with_bearing_and_distance
@dataclass
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,
"threshold": self.threshold
}
def project_with_bearing_and_distance(self, d, bearing):
"""
Project this LatLng point with a bearing and distance.
Args:
d: Distance in meters to project.
bearing: Bearing in radians.
Returns:
A new LatLng point projected from this point.
"""
(new_lat, new_lng) = project_with_bearing_and_distance(self.lat, self.lng, d, bearing)
return LatLng(new_lat, new_lng, self.alt)
def distance_to(self, other):
"""
Calculate the distance to another LatLng point.
Args:
other: Another LatLng point.
Returns:
Distance in meters to the other point.
"""
return distance(self.lat, self.lng, other.lat, other.lng)
def bearing_to(self, other):
"""
Calculate the bearing to another LatLng point.
Args:
other: Another LatLng point.
Returns:
Bearing in radians to the other point.
"""
return bearing_to(self.lat, self.lng, other.lat, other.lng)
@dataclass
class TACAN:
is_on: bool
channel: int
xy: str
callsign: str
@dataclass
class Radio:
frequency: int
callsign: int
callsign_number: int
@dataclass
class GeneralSettings:
prohibit_jettison: bool
prohibit_aa: bool
prohibit_ag: bool
prohibit_afterburner: bool
prohibit_air_wpn: bool
@dataclass
class Ammo:
quantity: int
name: str
guidance: int
category: int
missile_category: int
@dataclass
class Contact:
id: int
detection_method: int
@dataclass
class Offset:
x: float
y: float
z: float
@dataclass
class DrawArgument:
argument: int
value: float

View File

@@ -0,0 +1 @@
ROES = ["", "free", "designated", "return", "hold"]

View File

@@ -0,0 +1,19 @@
states = [
"none",
"idle",
"reach-destination",
"attack",
"follow",
"land",
"refuel",
"AWACS",
"tanker",
"bomb-point",
"carpet-bomb",
"bomb-building",
"fire-at-area",
"simulate-fire-fight",
"scenic-aaa",
"miss-on-purpose",
"land-at-point"
]

View File

@@ -0,0 +1,30 @@
from dataclasses import dataclass
from typing import Optional
from data.data_types import LatLng
@dataclass
class UnitSpawnTable:
"""Unit spawn table data structure for spawning units."""
unit_type: str
location: LatLng
skill: str
livery_id: str
altitude: Optional[int] = None
loadout: Optional[str] = None
heading: Optional[int] = None
def toJSON(self):
"""Convert the unit spawn table to a JSON serializable dictionary."""
return {
"unitType": self.unit_type,
"location": {
"lat": self.location.lat,
"lng": self.location.lng,
"alt": self.location.alt
},
"skill": self.skill,
"liveryID": self.livery_id,
"altitude": self.altitude,
"loadout": self.loadout,
"heading": self.heading
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
{
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,196 @@
import asyncio
from random import randrange
from api import API, Unit, UnitSpawnTable
from math import pi
# Setup a logger for the module
import logging
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 = []
#############################################################################################
# This class represents a disembarked infantry unit that will engage in combat
# after disembarking from a vehicle. It will move forward and engage the closest enemy.
#############################################################################################
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 start_fighting(self, random_bearing: bool = False):
"""
Start the fighting process for the unit. The unit will go forward 30 meters in the direction of the closest enemy and then start a firefight
with the closest enemy unit.
"""
logger.info(f"Unit {self.unit_id} is now fighting.")
# Pick a random target
target = self.pick_random_target()
if random_bearing:
# If random_bearing is True use a random bearing
bearing = randrange(0, 100) / 100 * pi * 2
elif target is None:
# If no target is found, use the unit's current heading
bearing = self.heading
else:
bearing = self.position.bearing_to(target.position)
# Project the unit's position 30 meters
destination = self.position.project_with_bearing_and_distance(30, bearing)
# Set the destination for the unit
self.set_path([destination])
# Register a callback for when the unit reaches its destination
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 pick_random_target(self):
# Find the closest enemy unit
targets = self.api.get_closest_units(
["neutral", "red" if self.coalition == "blue" else "blue"],
["groundunit"],
self.position,
"red" if self.coalition == "blue" else "blue",
10
)
# Pick a random enemy from the list
target = targets[randrange(len(targets))] if targets else None
return target
async def on_destination_reached(self, _, reached: bool):
if not reached:
logger.info(f"Unit {self} did not reach its destination.")
else:
logger.info(f"Unit {self} has reached its destination.")
target = self.pick_random_target()
if target is None:
logger.info("No enemies found nearby. Resuming patrol.")
await asyncio.sleep(1)
self.start_fighting(not reached) # Restart the fighting process, randomizing the bearing if not reached
else:
# Compute the bearing to the target
bearing_to_enemy = self.position.bearing_to(target.position)
# 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, firefight_destination.alt + 1)
await asyncio.sleep(10) # Simulate some time spent in firefight
self.start_fighting() # Restart the fighting process
#############################################################################################
# This function is called when the API starts up. It will delete all blue units that are not human and alive.
#############################################################################################
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 = []
# Delete the AI blue units
for unit in units.values():
if unit.alive and not unit.human and unit.coalition == "blue":
units_to_delete.append(unit)
try:
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:
logger.error(f"Failed to delete unit {unit}: {e}")
#################################################################################################
# This function is called when a unit's alive property changes. If the unit is deleted,
# it will be removed from the units_to_delete list. If all units are deleted, it will spawn a new unit.
#################################################################################################
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)
logger.info(f"Unit {unit} has been deleted and removed from the list.")
else:
logger.warning(f"Unit {unit} is not in the deletion list, but it is marked as dead.")
##############################################################################################
# This function is called when the API updates. It checks if all units have been deleted and
# if so, it spawns new units near a human unit that is alive and on the ground.
##############################################################################################
def on_api_update(api: API):
global units_to_delete
if units_to_delete is not None and len(units_to_delete) == 0:
logger.info("All units have been deleted successfully.")
units_to_delete = None
# Get the units from the API
logger.info("Spawning a disembarked infantry units.")
units = api.get_units()
# Find the first human unit that is alive and on the ground
for unit in units.values():
if unit.human and unit.alive and not unit.airborne:
for i in range(10):
# Spawn unit nearby
spawn_position = unit.position.project_with_bearing_and_distance(10, unit.heading + pi / 2 + 0.2 * i)
spawn_table: UnitSpawnTable = UnitSpawnTable(
unit_type="Soldier M4",
location=spawn_position,
heading=unit.heading + pi / 2 + 0.2 * i,
skill="High",
livery_id=""
)
# Define the callback for when the unit is spawned. This is an asynchronous function but could be synchronous too.
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.start_fighting()
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
##############################################################################################
# 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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -0,0 +1,85 @@
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("example_voice_control")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
# Function to handle received messages
# This function will be called when a message is received on the radio frequency
def on_message_received(recognized_text: str, unit_id: str, api: API, listener: RadioListener):
logger.info(f"Received message from {unit_id}: {recognized_text}")
units = api.update_units()
# Extract the unit that sent the message
if not units:
logger.warning("No units available in API, unable to process audio.")
return
if unit_id not in units:
logger.warning(f"Unit ID {unit_id} not found in API units, unable to process audio.")
return
unit = units[unit_id]
# Check for troop disembarkment request (expanded)
keywords = [
"disembark troops",
"deploy troops",
"unload troops",
"drop off troops",
"let troops out",
"troops disembark",
"troops out",
"extract infantry",
"release soldiers",
"disembark infantry",
"release troops"
]
is_disembarkment = any(kw in recognized_text.lower() for kw in keywords)
# Check if "olympus" is mentioned
is_olympus = "olympus" in recognized_text.lower()
if is_olympus and is_disembarkment:
logger.info("Troop disembarkment requested!")
# Use the API to spawn an infrantry unit 10 meters away from the unit
spawn_location = unit.position.project_with_bearing_and_distance(bearing=unit.heading+pi/2, d=10)
spawn_table: UnitSpawnTable = UnitSpawnTable(
unit_type="Soldier M4",
location=spawn_location,
heading=unit.heading+pi/2,
skill="High",
livery_id=""
)
api.spawn_ground_units([spawn_table], unit.coalition, "", True, 0)
message_filename = api.generate_audio_message("Roger, disembarking")
listener.transmit_on_frequency(message_filename, listener.frequency, listener.modulation, listener.encryption)
else:
logger.info("Did not understand the message or no disembarkment request found.")
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 recognized_text, unit_id, api=api, listener=listener: on_message_received(recognized_text, unit_id, api, listener))
api.run()

View File

@@ -0,0 +1,776 @@
import asyncio
from asyncio import Semaphore
from random import randrange
from api import API, Unit, UnitSpawnTable
from math import pi
import logging
import time
#Set some globals up
before_can_re_embark_time = 300 # this is the time it takes for the infantry, after disembarking, to become embarkable again
min_toggle_time_period = 30 # this should typically be however long it takes the longest thing to load or unload, used to prevent accidental re toggling the toggle switch too early by accident
####Transport types#####
transport_ground = {}
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
"centre_offset_position": [(0,0),(0,0)], #used for calculating the unit door centre, when the doors aren't in line with the centre
"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,
"rotor_radius": 15
},
"CH-47Fbl1":{
"max_capacity": 30,
"max_embark_range": 100,
"doors": 1,
"door_positions": [(12,-pi),(0,0)], #two values here offset and heading offset in radians and second distance offset and heading offset in radians
"centre_offset_position": [(11,-pi),(0,0)], #used for calculating the unit door centre, when the doors aren't in line with the centre
"door_argument_nos": [86], #draw argument numbers for the doors
"door_open_thresholds": [0.55], #value above which the door is considered open
"is_rear_loader": True,
"boarding_distance": 10,
"rotor_radius": 31
},
"Mi-8MT":{
"max_capacity": 24,
"max_embark_range": 100,
"doors": 1,
"door_positions": [(6,-pi),(0,0)], #two values here offset and heading offset in radians and second distance offset and heading offset in radians
"centre_offset_position": [(5.5,-pi),(0,0)], #used for calculating the unit door centre, when the doors aren't in line with the centre
"door_argument_nos": [86], #draw argument numbers for the doors
"door_open_thresholds": [0.8], #value above which the door is considered open
"is_rear_loader": True,
"boarding_distance": 10,
"rotor_radius": 22
},
"Mi-24P":{
"max_capacity": 8,
"max_embark_range": 100,
"doors": 2,
"door_positions": [(2.5,+pi/2),(1.2,0),(2.5,-pi/2),(1.2,0)], #two values here offset and heading offset in radians and second distance offset and heading offset in radians
"centre_offset_position": [(0,0),(0,0)], #used for calculating the unit door centre, when the doors aren't in line with the centre
"door_argument_nos": [38,86], #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,
"rotor_radius": 18
}
}
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_boarding")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
def cross_product_2d(v1, v2):
return v1[0] * v2[1] - v1[1] * v2[0]
def determine_side(self, door):
# Calculate relative vectors
vector_to_unit = [
self.position.lat - self.transport_unit.position.lat,
self.position.lng - self.transport_unit.position.lng
]
vector_to_door = [
door.lat - self.transport_unit.position.lat,
door.lng - self.transport_unit.position.lng
]
# Compute the 2D cross product
cross_z = cross_product_2d(vector_to_unit, vector_to_door)
# Determine the side based on the sign of the cross product
if cross_z > 0:
return True
elif cross_z < 0:
return False
else:
return True
class Transporter(Unit):
def __init__(self, Unit):
self.unit = Unit
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.current_max_capacity = 0
self.unit.doors = transport_helicopters["UH-1H"]["doors"]
self.unit.door_positions = transport_helicopters["UH-1H"]["door_positions"]
self.unit.centre_offset_position = transport_helicopters["UH-1H"]["centre_offset_position"]
self.unit.door_argument_nos = transport_helicopters["UH-1H"]["door_argument_nos"]
self.unit.door_open_thresholds = transport_helicopters["UH-1H"]["door_open_thresholds"]
self.unit.will_disembark = True
self.unit.register_draw_argument(43) #left door
self.unit.register_draw_argument(44) #right door
self.unit.register_draw_argument(446) #interior light colour switch, we use as a toggle
self.unit.loading_toggle_argument = 2 #this is the argument registered in the index on the draw arguments that controls loading state, have to trial and error what it is for each transport type
self.unit.disembark_embark_argument_toggle_argument_threshold = 0.7
self.unit.is_rear_loader = transport_helicopters["UH-1H"]["is_rear_loader"]
self.unit.rotor_radius = transport_helicopters["UH-1H"]["rotor_radius"]
elif self.unit.name == "CH-47Fbl1":
self.unit.max_capacity = transport_helicopters["CH-47Fbl1"]["max_capacity"]
self.unit.max_embark_range = transport_helicopters["CH-47Fbl1"]["max_embark_range"]
self.unit.boarding_distance = transport_helicopters["CH-47Fbl1"]["boarding_distance"]
self.unit.current_capacity = 0
self.unit.current_cargo_weight = 0
self.unit.unit_array = []
self.unit.en_boarding_queue = []
self.unit.current_max_capacity = 0
self.unit.doors = transport_helicopters["CH-47Fbl1"]["doors"]
self.unit.centre_offset_position = transport_helicopters["CH-47Fbl1"]["centre_offset_position"]
self.unit.door_positions = transport_helicopters["CH-47Fbl1"]["door_positions"]
self.unit.door_argument_nos = transport_helicopters["CH-47Fbl1"]["door_argument_nos"]
self.unit.door_open_thresholds = transport_helicopters["CH-47Fbl1"]["door_open_thresholds"]
self.unit.will_disembark = True
self.unit.register_draw_argument(86) #rear ramp
self.unit.register_draw_argument(606) #interior light colour switch, we use as a toggle
self.unit.loading_toggle_argument = 1 #this is the argument registered in the index on the draw arguments that controls loading state, have to trial and error what it is for each transport type
self.unit.disembark_embark_argument_toggle_argument_threshold = 0.5
self.unit.is_rear_loader = transport_helicopters["CH-47Fbl1"]["is_rear_loader"]
self.unit.rotor_radius = transport_helicopters["CH-47Fbl1"]["rotor_radius"]
elif self.unit.name == "Mi-8MT":
self.unit.max_capacity = transport_helicopters["Mi-8MT"]["max_capacity"]
self.unit.max_embark_range = transport_helicopters["Mi-8MT"]["max_embark_range"]
self.unit.boarding_distance = transport_helicopters["Mi-8MT"]["boarding_distance"]
self.unit.current_capacity = 0
self.unit.current_cargo_weight = 0
self.unit.unit_array = []
self.unit.en_boarding_queue = []
self.unit.current_max_capacity = 0
self.unit.doors = transport_helicopters["Mi-8MT"]["doors"]
self.unit.centre_offset_position = transport_helicopters["Mi-8MT"]["centre_offset_position"]
self.unit.door_positions = transport_helicopters["Mi-8MT"]["door_positions"]
self.unit.door_argument_nos = transport_helicopters["Mi-8MT"]["door_argument_nos"]
self.unit.door_open_thresholds = transport_helicopters["Mi-8MT"]["door_open_thresholds"]
self.unit.will_disembark = True
self.unit.register_draw_argument(86) #rear ramp
self.unit.register_draw_argument(133) #interior light colour switch, we use as a toggle
self.unit.loading_toggle_argument = 1 #this is the argument registered in the index on the draw arguments that controls loading state, have to trial and error what it is for each transport type
self.unit.disembark_embark_argument_toggle_argument_threshold = 0.8
self.unit.is_rear_loader = transport_helicopters["Mi-8MT"]["is_rear_loader"]
self.unit.rotor_radius = transport_helicopters["Mi-8MT"]["rotor_radius"]
elif self.unit.name == "Mi-24P":
self.unit.max_capacity = transport_helicopters["Mi-24P"]["max_capacity"]
self.unit.max_embark_range = transport_helicopters["Mi-24P"]["max_embark_range"]
self.unit.boarding_distance = transport_helicopters["Mi-24P"]["boarding_distance"]
self.unit.current_capacity = 0
self.unit.current_cargo_weight = 0
self.unit.unit_array = []
self.unit.en_boarding_queue = []
self.unit.current_max_capacity = 0
self.unit.doors = transport_helicopters["Mi-24P"]["doors"]
self.unit.centre_offset_position = transport_helicopters["Mi-24P"]["centre_offset_position"]
self.unit.door_positions = transport_helicopters["Mi-24P"]["door_positions"]
self.unit.door_argument_nos = transport_helicopters["Mi-24P"]["door_argument_nos"]
self.unit.door_open_thresholds = transport_helicopters["Mi-24P"]["door_open_thresholds"]
self.unit.will_disembark = True
self.unit.register_draw_argument(38) #left door
self.unit.register_draw_argument(86) #right door
self.unit.register_draw_argument(47) #interior light colour switch, we use as a toggle
self.unit.loading_toggle_argument = 2 #this is the argument registered in the index on the draw arguments that controls loading state, have to trial and error what it is for each transport type
self.unit.disembark_embark_argument_toggle_argument_threshold = 0.8
self.unit.is_rear_loader = transport_helicopters["Mi-24P"]["is_rear_loader"]
self.unit.rotor_radius = transport_helicopters["Mi-24P"]["rotor_radius"]
else:
pass
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(2)
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 2000m, approx rifle max range
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(10) #wait a bit before trying again
try:
if self.og_transport.is_rear_loader:
side_offset = self.position.project_with_bearing_and_distance(30,self.transport_spawn_heading-pi/2)
self.set_path([side_offset,new_patrol])
else:
self.set_path([new_patrol])
except AttributeError:
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)
new_patrol = self.position.project_with_bearing_and_distance(1000, self.transport_spawn_heading)
await asyncio.sleep(10) #wait a bit before trying again
try:
if self.og_transport.is_rear_loader:
side_offset = self.position.project_with_bearing_and_distance(30,self.transport_spawn_heading-pi/2)
self.set_path([side_offset,new_patrol])
else:
self.set_path([new_patrol])
except AttributeError:
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 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.")
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 = self.position.distance_to(door)
distance_centre_offset_position = self.position.distance_to(self.transport_unit.position.project_with_bearing_and_distance(self.transport_unit.centre_offset_position[0][0], self.transport_unit.centre_offset_position[0][1]))
if distance >= distance_centre_offset_position:
if determine_side(self,door): #right side
if self.transport_unit.is_rear_loader: # chinook rear loader
destination = door.project_with_bearing_and_distance(self.transport_unit.rotor_radius/3, self.transport_unit.heading-pi)
destination = destination.project_with_bearing_and_distance(self.transport_unit.rotor_radius/2, self.transport_unit.heading+pi/2)
destination.threshold = 2
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: # huey front loader
destination = door.project_with_bearing_and_distance(self.transport_unit.rotor_radius, self.transport_unit.heading)
destination = destination.project_with_bearing_and_distance(self.transport_unit.rotor_radius/2, self.transport_unit.heading+pi/2)
destination.threshold = 2
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: #left side
if self.transport_unit.is_rear_loader: # chinook rear loader
destination = door.project_with_bearing_and_distance(self.transport_unit.rotor_radius/3, self.transport_unit.heading-pi)
destination = destination.project_with_bearing_and_distance(self.transport_unit.rotor_radius/2, self.transport_unit.heading-pi/2)
destination.threshold = 2
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: # huey front loader
destination = door.project_with_bearing_and_distance(self.transport_unit.rotor_radius, self.transport_unit.heading)
destination = destination.project_with_bearing_and_distance(self.transport_unit.rotor_radius/2, self.transport_unit.heading-pi/2)
destination.threshold = 2
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:
destination = self.position.project_with_bearing_and_distance(distance+2, self.position.bearing_to(door))
#destination = destination.project_with_bearing_and_distance(self.transport_unit.rotor_radius, self.transport_unit.heading+pi/2)
destination.threshold = 2
self.set_path([destination,door])
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
def check_for_door_status(transporter):
if not hasattr(transporter, 'draw_arguments') or len(transporter.draw_arguments) < transporter.doors:
#logger.warning(f"Transporter '{transporter.name}' does not have enough draw arguments registered.")
return False
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 i >= len(transporter.draw_arguments): # Ensure the index is valid
#logger.error(f"Index {i} out of range for draw_arguments in transporter '{transporter.name}'.")
continue
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 i >= len(transporter.draw_arguments): # Ensure the index is valid
#logger.error(f"Index {i} out of range for draw_arguments in transporter '{transporter.name}'.")
continue
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:
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 or embarker.position.distance_to(embarker.transport_unit.position) < 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(closest_door) < 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.current_max_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.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):
transport.will_disembark = True
transport.en_boarding_queue = []
async def set_as_not_disembarking(transport):
transport.will_disembark = False
transport.current_max_capacity = transport.current_capacity
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 and transporter.current_capacity > 0: # check speed is less than 2 m/s and doors are open
# Transport is ready to disembark
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
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.og_transport = transporter
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
async def delayed_spawn(delay,transporter,open_doors,open_doors_headings,disembarker):
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=""
)
# Add a delay before spawning the unit
await asyncio.sleep(delay) # Delay of 2 seconds (adjust as needed)
api.spawn_ground_units([spawn_table], transporter.coalition, "", True, 0, execution_callback)
transporter.set_cargo_weight(transporter.current_cargo_weight - 100) # Assume 100kg per infantry with kit
transporter.current_cargo_weight -= 100
logger.info(f"Spawned unit '{disembarker.name}' from open door of transport '{transporter.name}'.")
if len(open_doors) > 1:
if (transporter.current_max_capacity - transporter.current_capacity) < len(open_doors):
delay = 0.1
else:
delay = (transporter.current_max_capacity - transporter.current_capacity) * 1.25 - ((len(open_doors)-1) * 2.5) + 2.5
else:
delay = (transporter.current_max_capacity - transporter.current_capacity) * 2.5
asyncio.create_task(delayed_spawn(delay,transporter,open_doors,open_doors_headings,disembarker))
transporter.en_boarding_queue = []
transporter.current_capacity -= 1
to_remove.append(disembarker)
for disembarker in to_remove:
transporter.unit_array.remove(disembarker)
except Exception as e:
#logging.warning(e, exc_info=True)
logger.info(f"Error in check_for_unloadable_units: {e}")
async def check_for_loadable_units():
units = api.get_units()
try:
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 and not hasattr(embarker, 'is_in_queue'):
transporter.en_boarding_queue.append(embarker)
embarker.in_embark_queue = True
embarker.is_in_queue = True
embarker.transport_unit = transporter
logger.info(f"Added embarker '{embarker.name}' to '{transporter.name}' s boarding queue.")
elif embarker not in transporter.en_boarding_queue and distance < transporter.max_embark_range and hasattr(embarker, 'is_in_queue'):
if embarker.is_in_queue:
await asyncio.sleep(60) #wait a bit and try again next time
embarker.is_in_queue = False
else:
transporter.en_boarding_queue.append(embarker)
embarker.in_embark_queue = True
embarker.is_in_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
except Exception as e:
logger.error(f"Error in check_for_loadable_units: {e}")
async def check_for_transport_embarker_or_disembark():
units = api.get_units()
try:
for transporter in units.values():
if transporter.alive and hasattr(transporter, 'is_transport'):
# Ensure the transporter has a `last_toggle_time` attribute
if not hasattr(transporter, 'last_toggle_time'):
transporter.last_toggle_time = 0 # Initialize it to 0
# Get the current time
current_time = time.time()
# Check if the toggle is allowed (min_toggle_time_period seconds since the last toggle)
if current_time - transporter.last_toggle_time < min_toggle_time_period:
continue # Skip toggling if the cooldown hasn't passed
# Check the loading toggle argument and toggle the state
if transporter.loading_toggle_argument is None or not hasattr(transporter, 'draw_arguments') or len(transporter.draw_arguments) <= transporter.loading_toggle_argument:
pass
else:
if transporter.will_disembark:
if transporter.draw_arguments[transporter.loading_toggle_argument].value <= transporter.disembark_embark_argument_toggle_argument_threshold:
continue
elif transporter.draw_arguments[transporter.loading_toggle_argument].value > transporter.disembark_embark_argument_toggle_argument_threshold:
# Set to embark
await set_as_not_disembarking(transporter)
transporter.last_toggle_time = current_time # Update the last toggle time
logger.info(f"Transporter '{transporter.name}' set to embark.")
else:
if transporter.draw_arguments[transporter.loading_toggle_argument].value <= transporter.disembark_embark_argument_toggle_argument_threshold:
# Set to disembark
await set_as_disembarking(transporter)
transporter.last_toggle_time = current_time # Update the last toggle time
logger.info(f"Transporter '{transporter.name}' set to disembark.")
elif transporter.draw_arguments[transporter.loading_toggle_argument].value > transporter.disembark_embark_argument_toggle_argument_threshold:
continue
except Exception as e:
logger.error(f"Error in check_for_transport_embarker_or_disembark: {e}")
#############
#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 = []
generate_transport_units() #comment this if doing draw Args testing
#new_test_unit() # comment this if running normally, this is used only for getting draw args
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
# unit_args = []
# exclusions_array = [1,102,103,11,12,17,278,279,280,281,282,283,284,286,287,288,289,290,337,37,39,393,399,4,40,41,42,448,487,488,6,77,99]
# async def check_args_changed():
# global unit_args
# units = api.get_units()
# for unit in units.values():
# for argument in unit.draw_arguments:
# if argument in unit_args:
# pass
# else:
# if argument.argument in exclusions_array:
# pass
# else:
# print(argument.argument, end=",")
# unit_args = unit.draw_arguments
# print("New loop")
# def new_test_unit():
# units = api.get_units()
# for unit in units.values():
# if unit.alive and unit.name in transport_types and not hasattr(unit, 'is_transport'):
# for i in range(500): #191
# unit.register_draw_argument(i)
# def check_arg_value():
# units = api.get_units()
# for unit in units.values():
# if unit.alive and unit.name in transport_types:
# unit.register_draw_argument(47) #191
# print(f"{unit.draw_arguments[0].argument} value is {unit.draw_arguments[0].value}")
async def on_api_update(api: API):
asyncio.create_task(check_for_loadable_units())
asyncio.create_task(load_loadable_units())
asyncio.create_task(check_for_unloadable_units())
asyncio.create_task(check_for_transport_embarker_or_disembark())
generate_transport_units()
#asyncio.create_task(check_args_changed())
#check_arg_value()
if __name__ == "__main__":
api = API()
api.register_on_startup_callback(on_api_startup)
api.register_on_update_callback(on_api_update)
api.run()

View File

@@ -0,0 +1,47 @@
{
"backend": {
"address": "localhost",
"port": 4512
},
"authentication": {
"gameMasterPassword": "a474219e5e9503c84d59500bb1bda3d9ade81e52d9fa1c234278770892a6dd74",
"blueCommanderPassword": "7d2e1ef898b21db7411f725a945b76ec8dcad340ed705eaf801bc82be6fe8a4a",
"redCommanderPassword": "abc5de7abdb8ed98f6d11d22c9d17593e339fde9cf4b9e170541b4f41af937e3"
},
"frontend": {
"port": 3000,
"autoconnectWhenLocal": true,
"customAuthHeaders": {
"enabled": false,
"username": "X-Authorized",
"group": "X-Group"
},
"elevationProvider": {
"provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip",
"username": null,
"password": null
},
"mapLayers": {
"ArcGIS Satellite": {
"urlTemplate": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
"minZoom": 1,
"maxZoom": 19,
"attribution": "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Mapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community"
},
"OpenStreetMap Mapnik": {
"urlTemplate": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"minZoom": 1,
"maxZoom": 20,
"attribution": "OpenStreetMap contributors"
}
},
"mapMirrors": {
"DCS Map (Official)": "https://maps.dcsolympus.com/maps",
"DCS Map (Alt.)": "https://refugees.dcsolympus.com/maps"
}
},
"audio": {
"SRSPort": 5002,
"WSPort": 4000
}
}

View File

@@ -0,0 +1,414 @@
"""
Audio Listener Module
WebSocket-based audio listener for real-time audio communication.
"""
import asyncio
import random
import websockets
import logging
import threading
from typing import Dict, Optional, Callable, Any
import json
from google.cloud import speech
from google.cloud.speech import SpeechContext
from audio.audio_packet import AudioPacket, MessageType
from audio.audio_recorder import AudioRecorder
from utils.utils import coalition_to_enum
import wave
import opuslib
import time
class RadioListener:
"""
WebSocket audio listener that connects to a specified address and port
to receive audio messages with graceful shutdown handling.
"""
def __init__(self, api, address: str = "localhost", port: int = 5000):
"""
Initialize the RadioListener.
Args:
address (str): WebSocket server address
port (int): WebSocket server port
message_callback: Optional callback function for handling received messages
"""
self.api = api
self.address = address
self.port = port
self.websocket_url = f"ws://{address}:{port}"
self.message_callback = None
self.clients_callback = None
self.frequency = 0
self.modulation = 0
self.encryption = 0
self.coalition = "blue"
self.speech_contexts = []
self.audio_recorders: Dict[str, AudioRecorder] = {}
# The guid is a random 22 char string, used to identify the radio
self._guid = ''.join(random.choice('abcdefghijklmnopqrstuvwxyz0123456789') for _ in range(22))
# Connection and control
self._websocket: Optional[websockets.WebSocketServerProtocol] = None
self._running = False
self._should_stop = False
self._loop: Optional[asyncio.AbstractEventLoop] = None
self._thread: Optional[threading.Thread] = None
# Clients data
self.clients_data: dict = {}
# Setup logging
self.logger = logging.getLogger(f"RadioListener-{address}:{port}")
if not self.logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO)
async def _handle_message(self, message: bytes) -> None:
"""
Handle received WebSocket message.
Args:
message: Raw message from WebSocket
"""
try:
# Extract the first byte to determine message type
message_type = message[0]
if message_type == MessageType.AUDIO.value:
audio_packet = AudioPacket()
audio_packet.from_byte_array(message[1:])
if audio_packet.get_transmission_guid() != self._guid:
if audio_packet.get_transmission_guid() not in self.audio_recorders:
recorder = AudioRecorder(self.api)
self.audio_recorders[audio_packet.get_transmission_guid()] = recorder
recorder.register_recording_callback(self._recording_callback)
self.audio_recorders[audio_packet.get_transmission_guid()].add_packet(audio_packet)
elif message_type == MessageType.CLIENTS_DATA.value:
clients_data = json.loads(message[1:])
self.clients_data = clients_data
if self.clients_callback:
self.clients_callback(clients_data)
except Exception as e:
self.logger.error(f"Error handling message: {e}")
def _recording_callback(self, wav_filename: str, unit_id: str) -> None:
"""
Callback for when audio data is recorded.
Args:
recorder: The AudioRecorder instance
audio_data: The recorded audio data
"""
if self.message_callback:
with open(wav_filename, 'rb') as audio_file:
audio_content = audio_file.read()
client = speech.SpeechClient()
config = speech.RecognitionConfig(
language_code="en",
encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
sample_rate_hertz=16000,
speech_contexts=[self.speech_contexts]
)
audio = speech.RecognitionAudio(content=audio_content)
# Synchronous speech recognition request
response = client.recognize(config=config, audio=audio)
# Extract recognized text
recognized_text = " ".join([result.alternatives[0].transcript for result in response.results])
self.message_callback(recognized_text, unit_id)
else:
self.logger.warning("No message callback registered to handle recorded audio")
async def _listen(self) -> None:
"""Main WebSocket listening loop."""
retry_count = 0
max_retries = 5
retry_delay = 2.0
while not self._should_stop and retry_count < max_retries:
try:
self.logger.info(f"Connecting to WebSocket at {self.websocket_url}")
async with websockets.connect(
self.websocket_url,
ping_interval=20,
ping_timeout=10,
close_timeout=10
) as websocket:
self._websocket = websocket
self._running = True
retry_count = 0 # Reset retry count on successful connection
self.logger.info("WebSocket connection established")
# Send the sync radio settings message
await self._sync_radio_settings()
# Listen for messages
async for message in websocket:
if self._should_stop:
break
await self._handle_message(message)
except websockets.exceptions.ConnectionClosed:
self.logger.warning("WebSocket connection closed")
if not self._should_stop:
retry_count += 1
if retry_count < max_retries:
self.logger.info(f"Retrying connection in {retry_delay} seconds... (attempt {retry_count}/{max_retries})")
await asyncio.sleep(retry_delay)
retry_delay = min(retry_delay * 1.5, 30.0) # Exponential backoff, max 30 seconds
else:
self.logger.error("Max retries reached, giving up")
break
except websockets.exceptions.InvalidURI:
self.logger.error(f"Invalid WebSocket URI: {self.websocket_url}")
break
except OSError as e:
self.logger.error(f"Connection error: {e}")
if not self._should_stop:
retry_count += 1
if retry_count < max_retries:
self.logger.info(f"Retrying connection in {retry_delay} seconds... (attempt {retry_count}/{max_retries})")
await asyncio.sleep(retry_delay)
retry_delay = min(retry_delay * 1.5, 30.0)
else:
self.logger.error("Max retries reached, giving up")
break
except Exception as e:
self.logger.error(f"Unexpected error in WebSocket listener: {e}")
break
self._running = False
self._websocket = None
self.logger.info("Audio listener stopped")
def _run_event_loop(self) -> None:
"""Run the asyncio event loop in a separate thread."""
try:
# Create new event loop for this thread
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
# Run the listener
self._loop.run_until_complete(self._listen())
except Exception as e:
self.logger.error(f"Error in event loop: {e}")
finally:
# Clean up
if self._loop and not self._loop.is_closed():
self._loop.close()
self._loop = None
async def _sync_radio_settings(self):
"""Send the radio settings of each radio to the SRS backend"""
message = {
"type": "Settings update",
"guid": self._guid,
"coalition": coalition_to_enum(self.coalition),
"settings": [
{
"frequency": self.frequency,
"modulation": self.modulation,
"ptt": False,
}
]
}
if self._websocket:
message_bytes = json.dumps(message).encode('utf-8')
data = bytes([MessageType.AUDIO.SETTINGS.value]) + message_bytes
await self._websocket.send(data)
async def _send_message(self, message: Any) -> bool:
"""
Send a message through the WebSocket connection.
Args:
message: Message to send (will be JSON-encoded if not a string)
Returns:
bool: True if message was sent successfully, False otherwise
"""
if not self.is_connected():
self.logger.warning("Cannot send message: WebSocket not connected")
return False
try:
# Convert message to string if needed
if isinstance(message, str):
data = message
else:
data = json.dumps(message)
await self._websocket.send(data)
self.logger.debug(f"Sent message: {data}")
return True
except Exception as e:
self.logger.error(f"Error sending message: {e}")
return False
def register_message_callback(self, callback: Callable[[str, str], None]) -> None:
"""Set the callback function for handling received messages.
Args:
callback (Callable[[str, str], None]): Function to call with recognized text and unit ID"""
self.message_callback = callback
def register_clients_callback(self, callback: Callable[[dict], None]) -> None:
"""Set the callback function for handling clients data."""
self.clients_callback = callback
def set_speech_contexts(self, contexts: list[SpeechContext]) -> None:
"""
Set the speech contexts for speech recognition.
Args:
contexts (list[SpeechContext]): List of SpeechContext objects
"""
self.speech_contexts = contexts
def start(self, frequency: int, modulation: int, encryption: int) -> None:
"""Start the audio listener in a separate thread.
Args:
frequency (int): Transmission frequency in Hz
modulation (int): Modulation type (0 for AM, 1 for FM, etc.)
encryption (int): Encryption type (0 for none, 1 for simple, etc., TODO)
"""
if self._running or self._thread is not None:
self.logger.warning("RadioListener is already running")
return
self._should_stop = False
self._thread = threading.Thread(target=self._run_event_loop, daemon=True)
self._thread.start()
self.logger.info(f"RadioListener started, connecting to {self.websocket_url}")
self.frequency = frequency
self.modulation = modulation
self.encryption = encryption
def transmit_on_frequency(self, file_name: str, frequency: float, modulation: int, encryption: int) -> bool:
"""
Transmit a WAV file as OPUS frames over the websocket.
Args:
file_name (str): Path to the input WAV file (linear16, mono, 16kHz)
frequency (float): Transmission frequency
modulation (int): Modulation type
encryption (int): Encryption type
Returns:
bool: True if transmission succeeded, False otherwise
"""
try:
# Open WAV file
with wave.open(file_name, 'rb') as wf:
if wf.getnchannels() != 1 or wf.getframerate() != 16000 or wf.getsampwidth() != 2:
self.logger.error("Input WAV must be mono, 16kHz, 16-bit (linear16)")
return False
frame_size = int(16000 * 0.04) # 40ms frames = 640 samples
encoder = opuslib.Encoder(16000, 1, opuslib.APPLICATION_AUDIO)
packet_id = 0
while True:
pcm_bytes = wf.readframes(frame_size)
if not pcm_bytes or len(pcm_bytes) < frame_size * 2:
break
# Encode PCM to OPUS
try:
opus_data = encoder.encode(pcm_bytes, frame_size)
except Exception as e:
self.logger.error(f"Opus encoding failed: {e}")
return False
# Create AudioPacket
packet = AudioPacket()
packet.set_packet_id(packet_id)
packet.set_audio_data(opus_data)
packet.set_frequencies([{
'frequency': frequency,
'modulation': modulation,
'encryption': encryption
}])
packet.set_transmission_guid(self._guid)
packet.set_client_guid(self._guid)
# Serialize and send over websocket
if self._websocket and self._loop and not self._loop.is_closed():
data = packet.to_byte_array()
fut = asyncio.run_coroutine_threadsafe(self._websocket.send(data), self._loop)
try:
fut.result(timeout=2.0)
except Exception as send_err:
self.logger.error(f"Failed to send packet {packet_id}: {send_err}")
return False
else:
self.logger.error("WebSocket not connected")
return False
packet_id += 1
time.sleep(0.04) # Simulate real-time transmission
self.logger.info(f"Transmitted {packet_id} packets from {file_name}")
return True
except Exception as e:
self.logger.error(f"Transmit failed: {e}")
return False
def stop(self) -> None:
"""Stop the audio listener gracefully."""
if not self._running and self._thread is None:
self.logger.info("RadioListener is not running")
return
self.logger.info("Stopping RadioListener...")
self._should_stop = True
# Close WebSocket connection if active
if self._websocket and self._loop:
# Schedule the close in the event loop
if not self._loop.is_closed():
asyncio.run_coroutine_threadsafe(self._websocket.close(), self._loop)
# Wait for thread to finish
if self._thread:
self._thread.join(timeout=5.0)
if self._thread.is_alive():
self.logger.warning("Thread did not stop gracefully within timeout")
self._thread = None
self._running = False
self.logger.info("RadioListener stopped")
def is_running(self) -> bool:
"""Check if the audio listener is currently running."""
return self._running
def is_connected(self) -> bool:
"""Check if WebSocket is currently connected."""
return self._websocket is not None and not self._websocket.closed
def __enter__(self):
"""Context manager entry."""
self.start()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit with graceful shutdown."""
self.stop()

View File

@@ -0,0 +1,803 @@
from typing import List
import asyncio
from data.data_extractor import DataExtractor
from data.data_indexes import DataIndexes
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
class Unit:
def __init__(self, id: int, api):
from api import API
self.ID = id
self.api: API = api
# Data controlled directly by the backend
self.category = ""
self.alive = False
self.alarm_state = "AUTO"
self.human = False
self.controlled = False
self.coalition = "neutral"
self.country = 0
self.name = ""
self.unit_name = ""
self.callsign = ""
self.group_id = 0
self.unit_id = 0
self.group_name = ""
self.state = ""
self.task = ""
self.has_task = False
self.position = LatLng(0, 0, 0)
self.speed = 0.0
self.horizontal_velocity = 0.0
self.vertical_velocity = 0.0
self.heading = 0.0
self.track = 0.0
self.is_active_tanker = False
self.is_active_awacs = False
self.on_off = True
self.follow_roads = False
self.fuel = 0
self.desired_speed = 0.0
self.desired_speed_type = "CAS"
self.desired_altitude = 0.0
self.desired_altitude_type = "ASL"
self.leader_id = 0
self.formation_offset = Offset(0, 0, 0)
self.target_id = 0
self.target_position = LatLng(0, 0, 0)
self.roe = ""
self.reaction_to_threat = ""
self.emissions_countermeasures = ""
self.tacan = TACAN(False, 0, "X", "TKR")
self.radio = Radio(124000000, 1, 1)
self.general_settings = GeneralSettings(False, False, False, False, False)
self.ammo: List[Ammo] = []
self.contacts: List[Contact] = []
self.active_path: List[LatLng] = []
self.is_leader = False
self.operate_as = "blue"
self.shots_scatter = 2
self.shots_intensity = 2
self.health = 100
self.racetrack_length = 0.0
self.racetrack_anchor = LatLng(0, 0, 0)
self.racetrack_bearing = 0.0
self.airborne = False
self.radar_state = False
self.time_to_next_tasking = 0.0
self.barrel_height = 0.0
self.muzzle_velocity = 0.0
self.aim_time = 0.0
self.shots_to_fire = 0
self.shots_base_interval = 0.0
self.shots_base_scatter = 0.0
self.engagement_range = 0.0
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.custom_string = ""
self.custom_integer = 0
self.previous_total_ammo = 0
self.total_ammo = 0
self.on_property_change_callbacks = {}
self.on_destination_reached_callback = None
self.destination = None
self.destination_reached_threshold = 10
self.destination_reached_timeout = None
self.destination_reached_start_time = None
def __repr__(self):
return f"Unit(id={self.ID}, name={self.name}, coalition={self.coalition}, position={self.position})"
def register_on_property_change_callback(self, property_name: str, callback):
"""
Register a callback function that will be called when a property changes.
Args:
property_name (str): The name of the property to watch.
callback (function): The function to call when the property changes. The callback should accept two parameters: the unit and the new value of the property.
"""
if property_name not in self.on_property_change_callbacks:
self.on_property_change_callbacks[property_name] = callback
def unregister_on_property_change_callback(self, property_name: str):
"""
Unregister a callback function for a property.
Args:
property_name (str): The name of the property to stop watching.
"""
if property_name in self.on_property_change_callbacks:
del self.on_property_change_callbacks[property_name]
def register_on_destination_reached_callback(self, callback, destination: LatLng, threshold: float = 10, timeout: float = None):
"""
Register a callback function that will be called when the unit reaches its destination.
If the destination is not reached within the specified timeout, the callback will also be called with `False`.
Args:
callback (function): The function to call when the destination is reached. The callback should accept two parameters: the unit and a boolean indicating whether the destination was reached.
destination (LatLng): The destination that the unit is expected to reach.
threshold (float): The distance threshold in meters to consider the destination reached. Default is 10 meters.
"""
self.on_destination_reached_callback = callback
self.destination = destination
self.destination_reached_threshold = threshold
self.destination_reached_timeout = timeout
self.destination_reached_start_time = asyncio.get_event_loop().time() if timeout else None
def unregister_on_destination_reached_callback(self):
"""
Unregister the callback function for destination reached.
"""
self.on_destination_reached_callback = None
self.destination = None
def _trigger_callback(self, property_name: str, value):
"""
Trigger a property change callback, executing it in the asyncio event loop if available.
Args:
property_name (str): The name of the property that changed.
value: The new value of the property.
"""
if property_name in self.on_property_change_callbacks:
callback = self.on_property_change_callbacks[property_name]
try:
# Try to get the current event loop and schedule the callback
loop = asyncio.get_running_loop()
loop.create_task(self._run_callback_async(callback, self, value))
except RuntimeError:
# No event loop running, execute synchronously
callback(self, value)
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
print(f"Error in property change callback: {e}")
def _trigger_destination_reached_callback(self, reached: bool):
"""
Trigger the destination reached callback, executing it in the asyncio event loop if available.
Args:
reached (bool): Whether the destination was reached or not.
"""
if self.on_destination_reached_callback:
try:
# Try to get the current event loop and schedule the callback
loop = asyncio.get_running_loop()
loop.create_task(self._run_callback_async(self.on_destination_reached_callback, self, reached))
except RuntimeError:
# No event loop running, execute synchronously
self.on_destination_reached_callback(self, reached)
def update_from_data_extractor(self, data_extractor: DataExtractor):
datum_index = 0
while datum_index != DataIndexes.END_OF_DATA.value:
datum_index = data_extractor.extract_uint8()
if datum_index == DataIndexes.CATEGORY.value:
category = data_extractor.extract_string()
if category != self.category:
self.category = category
# Trigger callbacks for property change
if "category" in self.on_property_change_callbacks:
self._trigger_callback("category", self.category)
elif datum_index == DataIndexes.ALIVE.value:
alive = data_extractor.extract_bool()
if alive != self.alive:
self.alive = alive
# Trigger callbacks for property change
if "alive" in self.on_property_change_callbacks:
self._trigger_callback("alive", self.alive)
elif datum_index == DataIndexes.RADAR_STATE.value:
radar_state = data_extractor.extract_bool()
if radar_state != self.radar_state:
self.radar_state = radar_state
# Trigger callbacks for property change
if "radar_state" in self.on_property_change_callbacks:
self._trigger_callback("radar_state", self.radar_state)
elif datum_index == DataIndexes.HUMAN.value:
human = data_extractor.extract_bool()
if human != self.human:
self.human = human
# Trigger callbacks for property change
if "human" in self.on_property_change_callbacks:
self._trigger_callback("human", self.human)
elif datum_index == DataIndexes.CONTROLLED.value:
controlled = data_extractor.extract_bool()
if controlled != self.controlled:
self.controlled = controlled
# Trigger callbacks for property change
if "controlled" in self.on_property_change_callbacks:
self._trigger_callback("controlled", self.controlled)
elif datum_index == DataIndexes.COALITION.value:
coalition = enum_to_coalition(data_extractor.extract_uint8())
if coalition != self.coalition:
self.coalition = coalition
# Trigger callbacks for property change
if "coalition" in self.on_property_change_callbacks:
self._trigger_callback("coalition", self.coalition)
elif datum_index == DataIndexes.COUNTRY.value:
country = data_extractor.extract_uint8()
if country != self.country:
self.country = country
# Trigger callbacks for property change
if "country" in self.on_property_change_callbacks:
self._trigger_callback("country", self.country)
elif datum_index == DataIndexes.NAME.value:
name = data_extractor.extract_string()
if name != self.name:
self.name = name
# Trigger callbacks for property change
if "name" in self.on_property_change_callbacks:
self._trigger_callback("name", self.name)
elif datum_index == DataIndexes.UNIT_NAME.value:
unit_name = data_extractor.extract_string()
if unit_name != self.unit_name:
self.unit_name = unit_name
# Trigger callbacks for property change
if "unit_name" in self.on_property_change_callbacks:
self._trigger_callback("unit_name", self.unit_name)
elif datum_index == DataIndexes.CALLSIGN.value:
callsign = data_extractor.extract_string()
if callsign != self.callsign:
self.callsign = callsign
# Trigger callbacks for property change
if "callsign" in self.on_property_change_callbacks:
self._trigger_callback("callsign", self.callsign)
elif datum_index == DataIndexes.UNIT_ID.value:
unit_id = data_extractor.extract_uint32()
if unit_id != self.unit_id:
self.unit_id = unit_id
# Trigger callbacks for property change
if "unit_id" in self.on_property_change_callbacks:
self._trigger_callback("unit_id", self.unit_id)
elif datum_index == DataIndexes.GROUP_ID.value:
group_id = data_extractor.extract_uint32()
if group_id != self.group_id:
self.group_id = group_id
# Trigger callbacks for property change
if "group_id" in self.on_property_change_callbacks:
self._trigger_callback("group_id", self.group_id)
elif datum_index == DataIndexes.GROUP_NAME.value:
group_name = data_extractor.extract_string()
if group_name != self.group_name:
self.group_name = group_name
# Trigger callbacks for property change
if "group_name" in self.on_property_change_callbacks:
self._trigger_callback("group_name", self.group_name)
elif datum_index == DataIndexes.STATE.value:
state = states[data_extractor.extract_uint8()]
if state != self.state:
self.state = state
# Trigger callbacks for property change
if "state" in self.on_property_change_callbacks:
self._trigger_callback("state", self.state)
elif datum_index == DataIndexes.TASK.value:
task = data_extractor.extract_string()
if task != self.task:
self.task = task
# Trigger callbacks for property change
if "task" in self.on_property_change_callbacks:
self._trigger_callback("task", self.task)
elif datum_index == DataIndexes.HAS_TASK.value:
has_task = data_extractor.extract_bool()
if has_task != self.has_task:
self.has_task = has_task
# Trigger callbacks for property change
if "has_task" in self.on_property_change_callbacks:
self._trigger_callback("has_task", self.has_task)
elif datum_index == DataIndexes.POSITION.value:
position = data_extractor.extract_lat_lng()
if position != self.position:
self.position = position
# Trigger callbacks for property change
if "position" in self.on_property_change_callbacks:
self._trigger_callback("position", self.position)
if self.on_destination_reached_callback and self.destination:
reached = self.position.distance_to(self.destination) < self.destination_reached_threshold
if reached or (
self.destination_reached_timeout and
(asyncio.get_event_loop().time() - self.destination_reached_start_time) > self.destination_reached_timeout
):
self._trigger_destination_reached_callback(reached)
self.unregister_on_destination_reached_callback()
elif datum_index == DataIndexes.SPEED.value:
speed = data_extractor.extract_float64()
if speed != self.speed:
self.speed = speed
# Trigger callbacks for property change
if "speed" in self.on_property_change_callbacks:
self._trigger_callback("speed", self.speed)
elif datum_index == DataIndexes.HORIZONTAL_VELOCITY.value:
horizontal_velocity = data_extractor.extract_float64()
if horizontal_velocity != self.horizontal_velocity:
self.horizontal_velocity = horizontal_velocity
# Trigger callbacks for property change
if "horizontal_velocity" in self.on_property_change_callbacks:
self._trigger_callback("horizontal_velocity", self.horizontal_velocity)
elif datum_index == DataIndexes.VERTICAL_VELOCITY.value:
vertical_velocity = data_extractor.extract_float64()
if vertical_velocity != self.vertical_velocity:
self.vertical_velocity = vertical_velocity
# Trigger callbacks for property change
if "vertical_velocity" in self.on_property_change_callbacks:
self._trigger_callback("vertical_velocity", self.vertical_velocity)
elif datum_index == DataIndexes.HEADING.value:
heading = data_extractor.extract_float64()
if heading != self.heading:
self.heading = heading
# Trigger callbacks for property change
if "heading" in self.on_property_change_callbacks:
self._trigger_callback("heading", self.heading)
elif datum_index == DataIndexes.TRACK.value:
track = data_extractor.extract_float64()
if track != self.track:
self.track = track
# Trigger callbacks for property change
if "track" in self.on_property_change_callbacks:
self._trigger_callback("track", self.track)
elif datum_index == DataIndexes.IS_ACTIVE_TANKER.value:
is_active_tanker = data_extractor.extract_bool()
if is_active_tanker != self.is_active_tanker:
self.is_active_tanker = is_active_tanker
# Trigger callbacks for property change
if "is_active_tanker" in self.on_property_change_callbacks:
self._trigger_callback("is_active_tanker", self.is_active_tanker)
elif datum_index == DataIndexes.IS_ACTIVE_AWACS.value:
is_active_awacs = data_extractor.extract_bool()
if is_active_awacs != self.is_active_awacs:
self.is_active_awacs = is_active_awacs
# Trigger callbacks for property change
if "is_active_awacs" in self.on_property_change_callbacks:
self._trigger_callback("is_active_awacs", self.is_active_awacs)
elif datum_index == DataIndexes.ON_OFF.value:
on_off = data_extractor.extract_bool()
if on_off != self.on_off:
self.on_off = on_off
# Trigger callbacks for property change
if "on_off" in self.on_property_change_callbacks:
self._trigger_callback("on_off", self.on_off)
elif datum_index == DataIndexes.FOLLOW_ROADS.value:
follow_roads = data_extractor.extract_bool()
if follow_roads != self.follow_roads:
self.follow_roads = follow_roads
# Trigger callbacks for property change
if "follow_roads" in self.on_property_change_callbacks:
self._trigger_callback("follow_roads", self.follow_roads)
elif datum_index == DataIndexes.FUEL.value:
fuel = data_extractor.extract_uint16()
if fuel != self.fuel:
self.fuel = fuel
# Trigger callbacks for property change
if "fuel" in self.on_property_change_callbacks:
self._trigger_callback("fuel", self.fuel)
elif datum_index == DataIndexes.DESIRED_SPEED.value:
desired_speed = data_extractor.extract_float64()
if desired_speed != self.desired_speed:
self.desired_speed = desired_speed
# Trigger callbacks for property change
if "desired_speed" in self.on_property_change_callbacks:
self._trigger_callback("desired_speed", self.desired_speed)
elif datum_index == DataIndexes.DESIRED_SPEED_TYPE.value:
desired_speed_type = "GS" if data_extractor.extract_bool() else "CAS"
if desired_speed_type != self.desired_speed_type:
self.desired_speed_type = desired_speed_type
# Trigger callbacks for property change
if "desired_speed_type" in self.on_property_change_callbacks:
self._trigger_callback("desired_speed_type", self.desired_speed_type)
elif datum_index == DataIndexes.DESIRED_ALTITUDE.value:
desired_altitude = data_extractor.extract_float64()
if desired_altitude != self.desired_altitude:
self.desired_altitude = desired_altitude
# Trigger callbacks for property change
if "desired_altitude" in self.on_property_change_callbacks:
self._trigger_callback("desired_altitude", self.desired_altitude)
elif datum_index == DataIndexes.DESIRED_ALTITUDE_TYPE.value:
desired_altitude_type = "AGL" if data_extractor.extract_bool() else "ASL"
if desired_altitude_type != self.desired_altitude_type:
self.desired_altitude_type = desired_altitude_type
# Trigger callbacks for property change
if "desired_altitude_type" in self.on_property_change_callbacks:
self._trigger_callback("desired_altitude_type", self.desired_altitude_type)
elif datum_index == DataIndexes.LEADER_ID.value:
leader_id = data_extractor.extract_uint32()
if leader_id != self.leader_id:
self.leader_id = leader_id
# Trigger callbacks for property change
if "leader_id" in self.on_property_change_callbacks:
self._trigger_callback("leader_id", self.leader_id)
elif datum_index == DataIndexes.FORMATION_OFFSET.value:
formation_offset = data_extractor.extract_offset()
if formation_offset != self.formation_offset:
self.formation_offset = formation_offset
# Trigger callbacks for property change
if "formation_offset" in self.on_property_change_callbacks:
self._trigger_callback("formation_offset", self.formation_offset)
elif datum_index == DataIndexes.TARGET_ID.value:
target_id = data_extractor.extract_uint32()
if target_id != self.target_id:
self.target_id = target_id
# Trigger callbacks for property change
if "target_id" in self.on_property_change_callbacks:
self._trigger_callback("target_id", self.target_id)
elif datum_index == DataIndexes.TARGET_POSITION.value:
target_position = data_extractor.extract_lat_lng()
if target_position != self.target_position:
self.target_position = target_position
# Trigger callbacks for property change
if "target_position" in self.on_property_change_callbacks:
self._trigger_callback("target_position", self.target_position)
elif datum_index == DataIndexes.ROE.value:
roe = ROES[data_extractor.extract_uint8()]
if roe != self.roe:
self.roe = roe
# Trigger callbacks for property change
if "roe" in self.on_property_change_callbacks:
self._trigger_callback("roe", self.roe)
elif datum_index == DataIndexes.ALARM_STATE.value:
alarm_state = self.enum_to_alarm_state(data_extractor.extract_uint8())
if alarm_state != self.alarm_state:
self.alarm_state = alarm_state
# Trigger callbacks for property change
if "alarm_state" in self.on_property_change_callbacks:
self._trigger_callback("alarm_state", self.alarm_state)
elif datum_index == DataIndexes.REACTION_TO_THREAT.value:
reaction_to_threat = self.enum_to_reaction_to_threat(data_extractor.extract_uint8())
if reaction_to_threat != self.reaction_to_threat:
self.reaction_to_threat = reaction_to_threat
# Trigger callbacks for property change
if "reaction_to_threat" in self.on_property_change_callbacks:
self._trigger_callback("reaction_to_threat", self.reaction_to_threat)
elif datum_index == DataIndexes.EMISSIONS_COUNTERMEASURES.value:
emissions_countermeasures = self.enum_to_emission_countermeasure(data_extractor.extract_uint8())
if emissions_countermeasures != self.emissions_countermeasures:
self.emissions_countermeasures = emissions_countermeasures
# Trigger callbacks for property change
if "emissions_countermeasures" in self.on_property_change_callbacks:
self._trigger_callback("emissions_countermeasures", self.emissions_countermeasures)
elif datum_index == DataIndexes.TACAN.value:
tacan = data_extractor.extract_tacan()
if tacan != self.tacan:
self.tacan = tacan
# Trigger callbacks for property change
if "tacan" in self.on_property_change_callbacks:
self._trigger_callback("tacan", self.tacan)
elif datum_index == DataIndexes.RADIO.value:
radio = data_extractor.extract_radio()
if radio != self.radio:
self.radio = radio
# Trigger callbacks for property change
if "radio" in self.on_property_change_callbacks:
self._trigger_callback("radio", self.radio)
elif datum_index == DataIndexes.GENERAL_SETTINGS.value:
general_settings = data_extractor.extract_general_settings()
if general_settings != self.general_settings:
self.general_settings = general_settings
# Trigger callbacks for property change
if "general_settings" in self.on_property_change_callbacks:
self._trigger_callback("general_settings", self.general_settings)
elif datum_index == DataIndexes.AMMO.value:
ammo = data_extractor.extract_ammo()
if ammo != self.ammo:
self.ammo = ammo
self.previous_total_ammo = self.total_ammo
self.total_ammo = sum(ammo.quantity for ammo in self.ammo)
# Trigger callbacks for property change
if "ammo" in self.on_property_change_callbacks:
self._trigger_callback("ammo", self.ammo)
elif datum_index == DataIndexes.CONTACTS.value:
contacts = data_extractor.extract_contacts()
if contacts != self.contacts:
self.contacts = contacts
# Trigger callbacks for property change
if "contacts" in self.on_property_change_callbacks:
self._trigger_callback("contacts", self.contacts)
elif datum_index == DataIndexes.ACTIVE_PATH.value:
active_path = data_extractor.extract_active_path()
if active_path != self.active_path:
self.active_path = active_path
# Trigger callbacks for property change
if "active_path" in self.on_property_change_callbacks:
self._trigger_callback("active_path", self.active_path)
elif datum_index == DataIndexes.IS_LEADER.value:
is_leader = data_extractor.extract_bool()
if is_leader != self.is_leader:
self.is_leader = is_leader
# Trigger callbacks for property change
if "is_leader" in self.on_property_change_callbacks:
self._trigger_callback("is_leader", self.is_leader)
elif datum_index == DataIndexes.OPERATE_AS.value:
operate_as = enum_to_coalition(data_extractor.extract_uint8())
if operate_as != self.operate_as:
self.operate_as = operate_as
# Trigger callbacks for property change
if "operate_as" in self.on_property_change_callbacks:
self._trigger_callback("operate_as", self.operate_as)
elif datum_index == DataIndexes.SHOTS_SCATTER.value:
shots_scatter = data_extractor.extract_uint8()
if shots_scatter != self.shots_scatter:
self.shots_scatter = shots_scatter
# Trigger callbacks for property change
if "shots_scatter" in self.on_property_change_callbacks:
self._trigger_callback("shots_scatter", self.shots_scatter)
elif datum_index == DataIndexes.SHOTS_INTENSITY.value:
shots_intensity = data_extractor.extract_uint8()
if shots_intensity != self.shots_intensity:
self.shots_intensity = shots_intensity
# Trigger callbacks for property change
if "shots_intensity" in self.on_property_change_callbacks:
self._trigger_callback("shots_intensity", self.shots_intensity)
elif datum_index == DataIndexes.HEALTH.value:
health = data_extractor.extract_uint8()
if health != self.health:
self.health = health
# Trigger callbacks for property change
if "health" in self.on_property_change_callbacks:
self._trigger_callback("health", self.health)
elif datum_index == DataIndexes.RACETRACK_LENGTH.value:
racetrack_length = data_extractor.extract_float64()
if racetrack_length != self.racetrack_length:
self.racetrack_length = racetrack_length
# Trigger callbacks for property change
if "racetrack_length" in self.on_property_change_callbacks:
self._trigger_callback("racetrack_length", self.racetrack_length)
elif datum_index == DataIndexes.RACETRACK_ANCHOR.value:
racetrack_anchor = data_extractor.extract_lat_lng()
if racetrack_anchor != self.racetrack_anchor:
self.racetrack_anchor = racetrack_anchor
# Trigger callbacks for property change
if "racetrack_anchor" in self.on_property_change_callbacks:
self._trigger_callback("racetrack_anchor", self.racetrack_anchor)
elif datum_index == DataIndexes.RACETRACK_BEARING.value:
racetrack_bearing = data_extractor.extract_float64()
if racetrack_bearing != self.racetrack_bearing:
self.racetrack_bearing = racetrack_bearing
# Trigger callbacks for property change
if "racetrack_bearing" in self.on_property_change_callbacks:
self._trigger_callback("racetrack_bearing", self.racetrack_bearing)
elif datum_index == DataIndexes.TIME_TO_NEXT_TASKING.value:
time_to_next_tasking = data_extractor.extract_float64()
if time_to_next_tasking != self.time_to_next_tasking:
self.time_to_next_tasking = time_to_next_tasking
# Trigger callbacks for property change
if "time_to_next_tasking" in self.on_property_change_callbacks:
self._trigger_callback("time_to_next_tasking", self.time_to_next_tasking)
elif datum_index == DataIndexes.BARREL_HEIGHT.value:
barrel_height = data_extractor.extract_float64()
if barrel_height != self.barrel_height:
self.barrel_height = barrel_height
# Trigger callbacks for property change
if "barrel_height" in self.on_property_change_callbacks:
self._trigger_callback("barrel_height", self.barrel_height)
elif datum_index == DataIndexes.MUZZLE_VELOCITY.value:
muzzle_velocity = data_extractor.extract_float64()
if muzzle_velocity != self.muzzle_velocity:
self.muzzle_velocity = muzzle_velocity
# Trigger callbacks for property change
if "muzzle_velocity" in self.on_property_change_callbacks:
self._trigger_callback("muzzle_velocity", self.muzzle_velocity)
elif datum_index == DataIndexes.AIM_TIME.value:
aim_time = data_extractor.extract_float64()
if aim_time != self.aim_time:
self.aim_time = aim_time
# Trigger callbacks for property change
if "aim_time" in self.on_property_change_callbacks:
self._trigger_callback("aim_time", self.aim_time)
elif datum_index == DataIndexes.SHOTS_TO_FIRE.value:
shots_to_fire = data_extractor.extract_uint32()
if shots_to_fire != self.shots_to_fire:
self.shots_to_fire = shots_to_fire
# Trigger callbacks for property change
if "shots_to_fire" in self.on_property_change_callbacks:
self._trigger_callback("shots_to_fire", self.shots_to_fire)
elif datum_index == DataIndexes.SHOTS_BASE_INTERVAL.value:
shots_base_interval = data_extractor.extract_float64()
if shots_base_interval != self.shots_base_interval:
self.shots_base_interval = shots_base_interval
# Trigger callbacks for property change
if "shots_base_interval" in self.on_property_change_callbacks:
self._trigger_callback("shots_base_interval", self.shots_base_interval)
elif datum_index == DataIndexes.SHOTS_BASE_SCATTER.value:
shots_base_scatter = data_extractor.extract_float64()
if shots_base_scatter != self.shots_base_scatter:
self.shots_base_scatter = shots_base_scatter
# Trigger callbacks for property change
if "shots_base_scatter" in self.on_property_change_callbacks:
self._trigger_callback("shots_base_scatter", self.shots_base_scatter)
elif datum_index == DataIndexes.ENGAGEMENT_RANGE.value:
engagement_range = data_extractor.extract_float64()
if engagement_range != self.engagement_range:
self.engagement_range = engagement_range
# Trigger callbacks for property change
if "engagement_range" in self.on_property_change_callbacks:
self._trigger_callback("engagement_range", self.engagement_range)
elif datum_index == DataIndexes.TARGETING_RANGE.value:
targeting_range = data_extractor.extract_float64()
if targeting_range != self.targeting_range:
self.targeting_range = targeting_range
# Trigger callbacks for property change
if "targeting_range" in self.on_property_change_callbacks:
self._trigger_callback("targeting_range", self.targeting_range)
elif datum_index == DataIndexes.AIM_METHOD_RANGE.value:
aim_method_range = data_extractor.extract_float64()
if aim_method_range != self.aim_method_range:
self.aim_method_range = aim_method_range
# Trigger callbacks for property change
if "aim_method_range" in self.on_property_change_callbacks:
self._trigger_callback("aim_method_range", self.aim_method_range)
elif datum_index == DataIndexes.ACQUISITION_RANGE.value:
acquisition_range = data_extractor.extract_float64()
if acquisition_range != self.acquisition_range:
self.acquisition_range = acquisition_range
# Trigger callbacks for property change
if "acquisition_range" in self.on_property_change_callbacks:
self._trigger_callback("acquisition_range", self.acquisition_range)
elif datum_index == DataIndexes.AIRBORNE.value:
airborne = data_extractor.extract_bool()
if airborne != self.airborne:
self.airborne = airborne
# 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)
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]):
return self.api.send_command({"setPath": {"ID": self.ID, "path": [latlng.toJSON() for latlng in path]}})
def attack_unit(self, target_id: int):
return self.api.send_command({"attackUnit": {"ID": self.ID, "targetID": target_id}})
def follow_unit(self, target_id: int, offset_x=0, offset_y=0, offset_z=0):
return self.api.send_command({"followUnit": {"ID": self.ID, "targetID": target_id, "offsetX": offset_x, "offsetY": offset_y, "offsetZ": offset_z}})
def delete_unit(self, explosion=False, explosion_type="", immediate=True):
return self.api.send_command({"deleteUnit": {"ID": self.ID, "explosion": explosion, "explosionType": explosion_type, "immediate": immediate}})
def land_at(self, location: LatLng):
return self.api.send_command({"landAt": {"ID": self.ID, "location": {"lat": location.lat, "lng": location.lng}}})
def change_speed(self, change: str):
return self.api.send_command({"changeSpeed": {"ID": self.ID, "change": change}})
def set_speed(self, speed: float):
return self.api.send_command({"setSpeed": {"ID": self.ID, "speed": speed}})
def set_speed_type(self, speed_type: str):
return self.api.send_command({"setSpeedType": {"ID": self.ID, "speedType": speed_type}})
def change_altitude(self, change: str):
return self.api.send_command({"changeAltitude": {"ID": self.ID, "change": change}})
def set_altitude_type(self, altitude_type: str):
return self.api.send_command({"setAltitudeType": {"ID": self.ID, "altitudeType": altitude_type}})
def set_altitude(self, altitude: float):
return self.api.send_command({"setAltitude": {"ID": self.ID, "altitude": altitude}})
def set_roe(self, roe: int):
return self.api.send_command({"setROE": {"ID": self.ID, "ROE": roe}})
def set_alarm_state(self, alarm_state: int):
return self.api.send_command({"setAlarmState": {"ID": self.ID, "alarmState": alarm_state}})
def set_reaction_to_threat(self, reaction_to_threat: int):
return self.api.send_command({"setReactionToThreat": {"ID": self.ID, "reactionToThreat": reaction_to_threat}})
def set_emissions_countermeasures(self, emissions_countermeasures: int):
return self.api.send_command({"setEmissionsCountermeasures": {"ID": self.ID, "emissionsCountermeasures": emissions_countermeasures}})
def set_on_off(self, on_off: bool):
return self.api.send_command({"setOnOff": {"ID": self.ID, "onOff": on_off}})
def set_follow_roads(self, follow_roads: bool):
return self.api.send_command({"setFollowRoads": {"ID": self.ID, "followRoads": follow_roads}})
def set_operate_as(self, operate_as: int):
return self.api.send_command({"setOperateAs": {"ID": self.ID, "operateAs": operate_as}})
def refuel(self):
return self.api.send_command({"refuel": {"ID": self.ID}})
def bomb_point(self, location: LatLng):
return self.api.send_command({"bombPoint": {"ID": self.ID, "location": {"lat": location.lat, "lng": location.lng}}})
def carpet_bomb(self, location: LatLng):
return self.api.send_command({"carpetBomb": {"ID": self.ID, "location": {"lat": location.lat, "lng": location.lng}}})
def bomb_building(self, location: LatLng):
return self.api.send_command({"bombBuilding": {"ID": self.ID, "location": {"lat": location.lat, "lng": location.lng}}})
def fire_at_area(self, location: LatLng):
return self.api.send_command({"fireAtArea": {"ID": self.ID, "location": {"lat": location.lat, "lng": location.lng}}})
def fire_laser(self, location: LatLng, code: int):
return self.api.send_command({"fireLaser": {"ID": self.ID, "location": {"lat": location.lat, "lng": location.lng}, "code": code}})
def fire_infrared(self, location: LatLng):
return self.api.send_command({"fireInfrared": {"ID": self.ID, "location": {"lat": location.lat, "lng": location.lng}}})
def simulate_fire_fight(self, location: LatLng, altitude: float):
return self.api.send_command({"simulateFireFight": {"ID": self.ID, "location": {"lat": location.lat, "lng": location.lng}, "altitude": altitude}})
def scenic_aaa(self, coalition: str):
return self.api.send_command({"scenicAAA": {"ID": self.ID, "coalition": coalition}})
def miss_on_purpose(self, coalition: str):
return self.api.send_command({"missOnPurpose": {"ID": self.ID, "coalition": coalition}})
def land_at_point(self, location: LatLng):
return self.api.send_command({"landAtPoint": {"ID": self.ID, "location": {"lat": location.lat, "lng": location.lng}}})
def set_shots_scatter(self, shots_scatter: int):
return self.api.send_command({"setShotsScatter": {"ID": self.ID, "shotsScatter": shots_scatter}})
def set_shots_intensity(self, shots_intensity: int):
return self.api.send_command({"setShotsIntensity": {"ID": self.ID, "shotsIntensity": shots_intensity}})
def set_racetrack(self, location: LatLng, bearing: float, length: float):
return self.api.send_command({"setRacetrack": {"ID": self.ID, "location": {"lat": location.lat, "lng": location.lng}, "bearing": bearing, "length": length}})
def set_advanced_options(self, is_active_tanker: bool, is_active_awacs: bool, tacan: dict, radio: dict, general_settings: dict):
return self.api.send_command({"setAdvancedOptions": {"ID": self.ID, "isActiveTanker": is_active_tanker, "isActiveAWACS": is_active_awacs, "TACAN": tacan, "radio": radio, "generalSettings": general_settings}})
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}})
def register_draw_argument(self, argument: int, active: bool = True):
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}})

View File

@@ -0,0 +1,83 @@
from math import asin, atan2, cos, degrees, radians, sin, sqrt
def enum_to_coalition(coalition_id: int) -> str:
if coalition_id == 0:
return "neutral"
elif coalition_id == 1:
return "red"
elif coalition_id == 2:
return "blue"
return ""
def coalition_to_enum(coalition: str) -> int:
if coalition == "neutral":
return 0
elif coalition == "red":
return 1
elif coalition == "blue":
return 2
return 0
def project_with_bearing_and_distance(lat1, lon1, d, bearing, R=6371000):
"""
lat: initial latitude, in degrees
lon: initial longitude, in degrees
d: target distance from initial in meters
bearing: (true) heading in radians
R: optional radius of sphere, defaults to mean radius of earth
Returns new lat/lon coordinate {d}m from initial, in degrees
"""
lat1 = radians(lat1)
lon1 = radians(lon1)
a = bearing
lat2 = asin(sin(lat1) * cos(d/R) + cos(lat1) * sin(d/R) * cos(a))
lon2 = lon1 + atan2(
sin(a) * sin(d/R) * cos(lat1),
cos(d/R) - sin(lat1) * sin(lat2)
)
return (degrees(lat2), degrees(lon2),)
def distance(lat1, lng1, lat2, lng2):
"""
Calculate the Haversine distance.
Args:
lat1: Latitude of the first point
lng1: Longitude of the first point
lat2: Latitude of the second point
lng2: Longitude of the second point
Returns:
Distance in meters between the two points.
"""
radius = 6371000
dlat = radians(lat2 - lat1)
dlon = radians(lng2 - lng1)
a = (sin(dlat / 2) * sin(dlat / 2) +
cos(radians(lat1)) * cos(radians(lat2)) *
sin(dlon / 2) * sin(dlon / 2))
c = 2 * atan2(sqrt(a), sqrt(1 - a))
d = radius * c
return d
def bearing_to(lat1, lng1, lat2, lng2):
"""
Calculate the bearing from one point to another.
Args:
lat1: Latitude of the first point
lng1: Longitude of the first point
lat2: Latitude of the second point
lng2: Longitude of the second point
Returns:
Bearing in radians from the first point to the second.
"""
dLon = (lng2 - lng1)
x = cos(radians(lat2)) * sin(radians(dLon))
y = cos(radians(lat1)) * sin(radians(lat2)) - sin(radians(lat1)) * cos(radians(lat2)) * cos(radians(dLon))
brng = atan2(x,y)
brng = brng
return brng

View File

@@ -1,3 +1,3 @@
{
"version": "v2.0.3"
"version": "v2.0.4"
}