mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Compare commits
34 Commits
v2.0.3
...
weapon-wiz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42280133c | ||
|
|
94d0b4d10e | ||
|
|
057603f926 | ||
|
|
bfd11c49af | ||
|
|
2a9723b932 | ||
|
|
f565b9ee6e | ||
|
|
504c0a0ed9 | ||
|
|
c77173f7c9 | ||
|
|
73af60d91b | ||
|
|
31d7fb6051 | ||
|
|
def15f5565 | ||
|
|
a257afca4b | ||
|
|
dca8f9189f | ||
|
|
3eef91fb24 | ||
|
|
73a7ea74f3 | ||
|
|
74b446d157 | ||
|
|
4e6701ff01 | ||
|
|
5fa1a26843 | ||
|
|
151196e5f2 | ||
|
|
716b0dc48d | ||
|
|
c66c9242b3 | ||
|
|
8404d4d956 | ||
|
|
37fa86dce8 | ||
|
|
4bcb5936b4 | ||
|
|
4fd9b7e6c2 | ||
|
|
e4af9b06d3 | ||
|
|
89bd39cea8 | ||
|
|
cd34eebcba | ||
|
|
07060112bc | ||
|
|
d6bcbaea7a | ||
|
|
42abb15aaf | ||
|
|
dcac0fd4f2 | ||
|
|
90d6acb7a4 | ||
|
|
2c6538663d |
@@ -28,9 +28,7 @@ Check the [Wiki](https://github.com/Pax1601/DCSOlympus/wiki) for installation in
|
||||
### I need troubleshooting guidance, please help? ###
|
||||
Read through the [Installation Guide](https://github.com/Pax1601/DCSOlympus/wiki) to ensure you have setup Olympus correctly.
|
||||
|
||||
Read through [Setup Troubleshooting](https://github.com/Pax1601/DCSOlympus/wiki/Setup-Troubleshooting) for common issues and solutions.
|
||||
|
||||
Read through the [Olympus User Guide](https://github.com/Pax1601/DCSOlympus/wiki/2.-User-Guide) to learn how to use Olympus.
|
||||
Read through [Setup Troubleshooting](https://github.com/Pax1601/DCSOlympus/wiki/3.-Setup-FAQ-and-Troubleshooting) for common issues and solutions.
|
||||
|
||||
If you're still having issues after trying the steps above, please post in the community-support channel with the following:
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -175,6 +185,7 @@ struct SpawnOptions {
|
||||
string skill;
|
||||
string liveryID;
|
||||
double heading;
|
||||
string payload;
|
||||
};
|
||||
|
||||
struct CloneOptions {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -101,6 +102,7 @@ string SpawnAircrafts::getString()
|
||||
<< "alt = " << spawnOptions[i].location.alt << ", "
|
||||
<< "heading = " << spawnOptions[i].heading << ", "
|
||||
<< "loadout = \"" << spawnOptions[i].loadout << "\"" << ", "
|
||||
<< "payload = " << spawnOptions[i].payload << ", "
|
||||
<< "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", "
|
||||
<< "skill = \"" << spawnOptions[i].skill << "\"" << "}, ";
|
||||
}
|
||||
@@ -113,6 +115,7 @@ string SpawnAircrafts::getString()
|
||||
<< "airbaseName = \"" << airbaseName << "\", "
|
||||
<< "country = \"" << country << "\", "
|
||||
<< "units = " << "{" << unitsSS.str() << "}" << "}";
|
||||
commandSS << ", \"" << this->getHash() << "\"";
|
||||
return commandSS.str();
|
||||
}
|
||||
|
||||
@@ -130,6 +133,7 @@ string SpawnHelicopters::getString()
|
||||
<< "alt = " << spawnOptions[i].location.alt << ", "
|
||||
<< "heading = " << spawnOptions[i].heading << ", "
|
||||
<< "loadout = \"" << spawnOptions[i].loadout << "\"" << ", "
|
||||
<< "payload = " << spawnOptions[i].payload << ", "
|
||||
<< "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", "
|
||||
<< "skill = \"" << spawnOptions[i].skill << "\"" << "}, ";
|
||||
}
|
||||
@@ -142,6 +146,7 @@ string SpawnHelicopters::getString()
|
||||
<< "airbaseName = \"" << airbaseName << "\", "
|
||||
<< "country = \"" << country << "\", "
|
||||
<< "units = " << "{" << unitsSS.str() << "}" << "}";
|
||||
commandSS << ", \"" << this->getHash() << "\"";
|
||||
return commandSS.str();
|
||||
}
|
||||
|
||||
@@ -315,4 +320,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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,12 @@ 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);
|
||||
string payload = "nil";
|
||||
if (unit.has_string_field(L"payload"))
|
||||
payload = to_string(unit[L"payload"]);
|
||||
|
||||
spawnOptions.push_back({ unitType, location, loadout, skill, liveryID, heading, payload });
|
||||
log(username + " spawned a " + coalition + " " + unitType, true);
|
||||
}
|
||||
|
||||
if (key.compare("spawnAircrafts") == 0)
|
||||
@@ -248,8 +261,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 +408,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 +437,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 +567,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 +592,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 +716,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 +822,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 +832,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);
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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()) {
|
||||
@@ -257,6 +271,8 @@ void Unit::getData(stringstream& ss, unsigned long long time)
|
||||
appendNumeric(ss, datumIndex, alive);
|
||||
datumIndex = DataIndex::unitID;
|
||||
appendNumeric(ss, datumIndex, unitID);
|
||||
datumIndex = DataIndex::groupID;
|
||||
appendNumeric(ss, datumIndex, groupID);
|
||||
}
|
||||
else {
|
||||
for (unsigned char datumIndex = DataIndex::startOfData + 1; datumIndex < DataIndex::lastIndex; datumIndex++)
|
||||
@@ -328,6 +344,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 +719,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 +803,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 +813,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
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
File diff suppressed because it is too large
Load Diff
BIN
docs/images/image.png
Normal file
BIN
docs/images/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/images/mods1.png
Normal file
BIN
docs/images/mods1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
720
docs/mods.json
Normal file
720
docs/mods.json
Normal file
@@ -0,0 +1,720 @@
|
||||
{
|
||||
"A-4E-C": {
|
||||
"loadouts": [
|
||||
{
|
||||
"name": "FFAR Mk1 HE *76, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "FFAR Mk1 HE", "quantity": 76 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "FFAR Mk1 HE *76, Fuel 300G",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-82 SE *12",
|
||||
"items": [{ "name": "Mk-82 Snakeye", "quantity": 12 }],
|
||||
"enabled": true,
|
||||
"code": "Mk-82 SE *12",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-82 *6, Fuel 150G *2",
|
||||
"items": [
|
||||
{ "name": "Mk-82", "quantity": 6 },
|
||||
{ "name": "Fuel Tank 150G", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-82 *6, Fuel 150G *2",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-82 SE *6, Fuel 150G *2",
|
||||
"items": [
|
||||
{ "name": "Mk-82 Snakeye", "quantity": 6 },
|
||||
{ "name": "Fuel Tank 150G", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-82 SE *6, Fuel 150G *2",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-83 *3, Fuel 300G *2",
|
||||
"items": [
|
||||
{ "name": "Mk-83", "quantity": 3 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-83 *3, Fuel 300G *2",
|
||||
"roles": ["Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-84 *3",
|
||||
"items": [{ "name": "Mk-84", "quantity": 3 }],
|
||||
"enabled": true,
|
||||
"code": "Mk-84 *3",
|
||||
"roles": ["Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-83 *5",
|
||||
"items": [{ "name": "Mk-83", "quantity": 5 }],
|
||||
"enabled": true,
|
||||
"code": "Mk-83 *5",
|
||||
"roles": ["Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-84 *3, Mk-82 *2",
|
||||
"items": [
|
||||
{ "name": "Mk-84", "quantity": 3 },
|
||||
{ "name": "Mk-82", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-84 *3, Mk-82 *2",
|
||||
"roles": ["Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-82 SE *8, Mk-81 SE *10",
|
||||
"items": [
|
||||
{ "name": "Mk-82 Snakeye", "quantity": 8 },
|
||||
{ "name": "Mk-81 Snakeye", "quantity": 10 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-82 SE *8, Mk-81 SE *10",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-81 *18",
|
||||
"items": [{ "name": "Mk-81", "quantity": 18 }],
|
||||
"enabled": true,
|
||||
"code": "Mk-81 *18",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-77 mod 0 *2, Mk-77 mod 1 *4",
|
||||
"items": [
|
||||
{ "name": "Mk-77 mod 0", "quantity": 2 },
|
||||
{ "name": "Mk-77 mod 1", "quantity": 4 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-77 mod 0 *2, Mk-77 mod 1 *4",
|
||||
"roles": ["Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-82 *6, LAU-10 *4",
|
||||
"items": [
|
||||
{ "name": "Mk-82", "quantity": 6 },
|
||||
{ "name": "LAU-10", "quantity": 4 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-82 *6, LAU-10 *4",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-4 HIPEG *2, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "Mk-4 HIPEG", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-4 HIPEG *2, Fuel 300G",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "LAU-10 *2, FFAR Mk1 HE *38, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "LAU-10", "quantity": 2 },
|
||||
{ "name": "FFAR Mk1 HE", "quantity": 38 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "LAU-10 *2, FFAR Mk1 HE *38, Fuel 300G",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "FFAR Mk5 HEAT *76, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "FFAR Mk5 HEAT", "quantity": 76 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "FFAR Mk5 HEAT *76, Fuel 300G",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "AGM-45B *2, LAU-10 *2, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "AGM-45B", "quantity": 2 },
|
||||
{ "name": "LAU-10", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "AGM-45B *2, LAU-10 *2, Fuel 300G",
|
||||
"roles": ["SEAD"]
|
||||
},
|
||||
{
|
||||
"name": "AGM-45B *4, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "AGM-45B", "quantity": 4 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "AGM-45B *4, Fuel 300G",
|
||||
"roles": ["SEAD"]
|
||||
},
|
||||
{
|
||||
"name": "CBU-2/A *2, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "CBU-2/A", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "CBU-2/A *2, Fuel 300G",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-4 HIPEG *3, Mk-82SE *2",
|
||||
"items": [
|
||||
{ "name": "Mk-4 HIPEG", "quantity": 3 },
|
||||
{ "name": "Mk-82 Snakeye", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-4 HIPEG *3, Mk-82SE *2",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-81 SE *6, LAU-10 *2, Fuel 150G *2",
|
||||
"items": [
|
||||
{ "name": "Mk-81 Snakeye", "quantity": 6 },
|
||||
{ "name": "LAU-10", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 150G", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-81 SE *6, LAU-10 *2, Fuel 150G *2",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "AGM-45B *4",
|
||||
"items": [{ "name": "AGM-45B", "quantity": 4 }],
|
||||
"enabled": true,
|
||||
"code": "AGM-45B *4",
|
||||
"roles": ["SEAD"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-83 *5, Mk-82 *2",
|
||||
"items": [
|
||||
{ "name": "Mk-83", "quantity": 5 },
|
||||
{ "name": "Mk-82", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-83 *5, Mk-82 *2",
|
||||
"roles": ["Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-81 SE *18",
|
||||
"items": [{ "name": "Mk-81 Snakeye", "quantity": 18 }],
|
||||
"enabled": true,
|
||||
"code": "Mk-81 SE *18",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-81 SE *12, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "Mk-81 Snakeye", "quantity": 12 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-81 SE *12, Fuel 300G",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-84 *2, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "Mk-84", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-84 *2, Fuel 300G",
|
||||
"roles": ["Strike"]
|
||||
},
|
||||
{
|
||||
"name": "CBU-2/A *2, Mk-82 SE *2, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "CBU-2/A", "quantity": 2 },
|
||||
{ "name": "Mk-82 Snakeye", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "CBU-2/A *2, Mk-82 SE *2, Fuel 300G",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-4 HIPEG *3, LAU-10 *2",
|
||||
"items": [
|
||||
{ "name": "Mk-4 HIPEG", "quantity": 3 },
|
||||
{ "name": "LAU-10", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-4 HIPEG *3, LAU-10 *2",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-81 SE *10, LAU-10 *2, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "Mk-81 Snakeye", "quantity": 10 },
|
||||
{ "name": "LAU-10", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-81 SE *10, LAU-10 *2, Fuel 300G",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "AGM-45B *2",
|
||||
"items": [{ "name": "AGM-45B", "quantity": 2 }],
|
||||
"enabled": true,
|
||||
"code": "AGM-45B *2",
|
||||
"roles": ["SEAD"]
|
||||
},
|
||||
{
|
||||
"name": "GAR-8 *2, Fuel 150G",
|
||||
"items": [
|
||||
{ "name": "GAR-8", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 150G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "GAR-8 *2, Fuel 150G",
|
||||
"roles": ["CAP"]
|
||||
},
|
||||
{
|
||||
"name": "AGM-45B *2, Fuel 300G *2",
|
||||
"items": [
|
||||
{ "name": "AGM-45B", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "AGM-45B *2, Fuel 300G *2",
|
||||
"roles": ["SEAD"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-82 *12",
|
||||
"items": [{ "name": "Mk-82", "quantity": 12 }],
|
||||
"enabled": true,
|
||||
"code": "Mk-82 *12",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "CBU-2/A *2, Mk-20 *2, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "CBU-2/A", "quantity": 2 },
|
||||
{ "name": "Mk-20", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "CBU-2/A *2, Mk-20 *2, Fuel 300G",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-4 HIPEG *3",
|
||||
"items": [{ "name": "Mk-4 HIPEG", "quantity": 3 }],
|
||||
"enabled": true,
|
||||
"code": "Mk-4 HIPEG *3",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "AGM-45B *2, LAU-10 *2",
|
||||
"items": [
|
||||
{ "name": "AGM-45B", "quantity": 2 },
|
||||
{ "name": "LAU-10", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "AGM-45B *2, LAU-10 *2",
|
||||
"roles": ["SEAD"]
|
||||
},
|
||||
{
|
||||
"name": "FFAR M156 WP *38, M257 Illumination *14, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "FFAR M156 WP", "quantity": 38 },
|
||||
{ "name": "M257 Illumination", "quantity": 14 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "FFAR M156 WP *38, M257 Illumination *14, Fuel 300G",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "FFAR M156 WP *38, M257 Illumination *14, Mk-82 SE *6",
|
||||
"items": [
|
||||
{ "name": "FFAR M156 WP", "quantity": 38 },
|
||||
{ "name": "M257 Illumination", "quantity": 14 },
|
||||
{ "name": "Mk-82 Snakeye", "quantity": 6 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "FFAR M156 WP *38, M257 Illumination *14, Mk-82 SE *6",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "FFAR M156 WP *38, Mk-82 SE *2, Mk-4 HIPEG",
|
||||
"items": [
|
||||
{ "name": "FFAR M156 WP", "quantity": 38 },
|
||||
{ "name": "Mk-82 Snakeye", "quantity": 2 },
|
||||
{ "name": "Mk-4 HIPEG", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "FFAR M156 WP *38, Mk-82 SE *2, Mk-4 HIPEG",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-4 HIPEG *2, FFAR M156 WP *19, LAU-10, M257 Illumination *7",
|
||||
"items": [
|
||||
{ "name": "Mk-4 HIPEG", "quantity": 2 },
|
||||
{ "name": "FFAR M156 WP", "quantity": 19 },
|
||||
{ "name": "LAU-10", "quantity": 1 },
|
||||
{ "name": "M257 Illumination", "quantity": 7 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-4 HIPEG *2, FFAR M156 WP *19, LAU-10, M257 Illumination *7",
|
||||
"roles": ["CAS"]
|
||||
},
|
||||
{
|
||||
"name": "Fuel 300G *3 (Ferry)",
|
||||
"items": [{ "name": "Fuel Tank 300G", "quantity": 3 }],
|
||||
"enabled": true,
|
||||
"code": "Fuel 300G *3 (Ferry)",
|
||||
"roles": ["Ferry"]
|
||||
},
|
||||
{
|
||||
"name": "CBU-2/A *2, LAU-10 *2, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "CBU-2/A", "quantity": 2 },
|
||||
{ "name": "LAU-10", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "CBU-2/A *2, LAU-10 *2, Fuel 300G",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-82 *8, Fuel 150G *2",
|
||||
"items": [
|
||||
{ "name": "Mk-82", "quantity": 8 },
|
||||
{ "name": "Fuel Tank 150G", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-82 *8, Fuel 150G *2",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "Mk-82 SE *8, Fuel 150G *2",
|
||||
"items": [
|
||||
{ "name": "Mk-82 Snakeye", "quantity": 8 },
|
||||
{ "name": "Fuel Tank 150G", "quantity": 2 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "Mk-82 SE *8, Fuel 150G *2",
|
||||
"roles": ["CAS", "Strike"]
|
||||
},
|
||||
{
|
||||
"name": "GAR-8 *2, Fuel 300G",
|
||||
"items": [
|
||||
{ "name": "GAR-8", "quantity": 2 },
|
||||
{ "name": "Fuel Tank 300G", "quantity": 1 }
|
||||
],
|
||||
"enabled": true,
|
||||
"code": "GAR-8 *2, Fuel 300G",
|
||||
"roles": ["CAP"]
|
||||
}
|
||||
],
|
||||
"name": "A-4E-C",
|
||||
"coalition": "blue",
|
||||
"era": "Mid Cold War",
|
||||
"category": "aircraft",
|
||||
"label": "A-4E-C Skyhawk",
|
||||
"shortLabel": "A4E",
|
||||
"filename": "a-4.png",
|
||||
"enabled": true,
|
||||
"type": "Aircraft",
|
||||
"description": "1 jet engine, straight wing, 1 crew, attack aircraft. Skyhawk",
|
||||
"abilities": "",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"length": 53,
|
||||
"liveries": {
|
||||
"aggressor usmc topgun mig-17": {
|
||||
"name": "USMC Aggressor TOPGUN 56 (MiG-17), 1982",
|
||||
"countries": ["AUSAF"]
|
||||
},
|
||||
"aggressor usn topgun": {
|
||||
"name": "USN Aggressor TOPGUN, 1982",
|
||||
"countries": ["AUSAF"]
|
||||
},
|
||||
"aggressor usn vf-126 bandits": {
|
||||
"name": "USN Aggressor VF-126 Bandits, 1976",
|
||||
"countries": ["AUSAF"]
|
||||
},
|
||||
"aggressor usn vf-127 royal blues": {
|
||||
"name": "USN Aggressor VF-127 Royal Blues, 1982",
|
||||
"countries": ["AUSAF"]
|
||||
},
|
||||
"aggressor usn vfa-127 cyclons (forest)": {
|
||||
"name": "USN Aggressor VFA-127 Cyclons (Forest), 1986",
|
||||
"countries": ["AUSAF"]
|
||||
},
|
||||
"aggressor usn vfa-127 cyclons (sea)": {
|
||||
"name": "USN Aggressor VFA-127 Cyclons (Sea), 1987",
|
||||
"countries": ["AUSAF"]
|
||||
},
|
||||
"blue angels no 1": {
|
||||
"name": "Blue Angels no. 1, 1986",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"blue angels no 2": {
|
||||
"name": "Blue Angels no. 2, 1986",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"blue angels no 3": {
|
||||
"name": "Blue Angels no. 3, 1986",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"blue angels no 4": {
|
||||
"name": "Blue Angels no. 4, 1986",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"blue angels no 5": {
|
||||
"name": "Blue Angels no. 5, 1986",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"blue angels no 6": {
|
||||
"name": "Blue Angels no. 6, 1986",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"community a-4e": {
|
||||
"name": "Community A-4E I: Autumn Splinter, 2018 (Fictional)",
|
||||
"countries": "All"
|
||||
},
|
||||
"community a-4e ii": {
|
||||
"name": "Community A-4E II: Winter Splinter, 2019 (Fictional)",
|
||||
"countries": "All"
|
||||
},
|
||||
"community a-4e iii": {
|
||||
"name": "Community A-4E III: Forever Free, 2021 (Fictional)",
|
||||
"countries": "All"
|
||||
},
|
||||
"community a-4e iv": {
|
||||
"name": "Community A-4E IV: Sea Otter, 2021 (Fictional)",
|
||||
"countries": "All"
|
||||
},
|
||||
"community a-4e v-1": {
|
||||
"name": "Community A-4E V: Blue Team, 2022 (Fictional)",
|
||||
"countries": "All"
|
||||
},
|
||||
"community a-4e v-2": {
|
||||
"name": "Community A-4E V: Red Team, 2022 (Fictional)",
|
||||
"countries": "All"
|
||||
},
|
||||
"international argentina": {
|
||||
"name": "Argentina ARA 309, 1982",
|
||||
"countries": ["ARG"]
|
||||
},
|
||||
"international argentina brigada iv": {
|
||||
"name": "Argentina FAA Falklands Brigada IV, 1982",
|
||||
"countries": ["ARG"]
|
||||
},
|
||||
"international argentina brigada v": {
|
||||
"name": "Argentina FAA Falklands Brigada V, 1982",
|
||||
"countries": ["ARG"]
|
||||
},
|
||||
"international australia": {
|
||||
"name": "Australian Navy, 1972",
|
||||
"countries": ["AUS"]
|
||||
},
|
||||
"international australia squadron 805": {
|
||||
"name": "Australian Navy Squadron 805, 1972",
|
||||
"countries": ["AUS"]
|
||||
},
|
||||
"international brazil": {
|
||||
"name": "Brazil Marinha do Brasil VF-1 Falcoes, 1998",
|
||||
"countries": ["BRA"]
|
||||
},
|
||||
"international brazil loviz": {
|
||||
"name": "Brazil Marinha do Brasil VF-1 LoViz, 2018",
|
||||
"countries": ["BRA"]
|
||||
},
|
||||
"international brazil vf-1 15 anos": {
|
||||
"name": "Brazil Marinha do Brasil VF-1 15 ANOS, 2013",
|
||||
"countries": ["BRA"]
|
||||
},
|
||||
"international finnish air force": {
|
||||
"name": "Finland FiAF, 1984 (Fictional)",
|
||||
"countries": ["FIN"]
|
||||
},
|
||||
"international israel": {
|
||||
"name": "Israel IAF Knights of the North Squadron, 1973",
|
||||
"countries": ["ISR"]
|
||||
},
|
||||
"international kuwait": {
|
||||
"name": "Kuwait KAF Free Kuwait, 1991",
|
||||
"countries": ["KWT"]
|
||||
},
|
||||
"international malaysia tudm": {
|
||||
"name": "Malaysia TUDM M32-29 No.9 Squadron, 2004",
|
||||
"countries": ["MYS"]
|
||||
},
|
||||
"international new zealand 50th": {
|
||||
"name": "RNZAF Golden T-Bird No.2, Squadron Ohakea, 1986",
|
||||
"countries": ["AUS"]
|
||||
},
|
||||
"international new zealand kahu": {
|
||||
"name": "RNZAF Kahu, 1988",
|
||||
"countries": ["AUS"]
|
||||
},
|
||||
"international new zealand kiwi red": {
|
||||
"name": "RNZAF Kiwi Red, 2011",
|
||||
"countries": ["AUS"]
|
||||
},
|
||||
"international new zealand sqn 75": {
|
||||
"name": "RNZAF Squadron 75, 1982",
|
||||
"countries": ["AUS"]
|
||||
},
|
||||
"trainer bae systems": {
|
||||
"name": "BAE Systems, 2013",
|
||||
"countries": ["UK"]
|
||||
},
|
||||
"trainer raf epts raspberry ripple": {
|
||||
"name": "RAF Empire Pilots Test School Raspberry Ripple, 2021 (Fictional)",
|
||||
"countries": ["UK"]
|
||||
},
|
||||
"trainer usmc nwef": {
|
||||
"name": "USMC Naval Weapons Evaluation Facility, 1974",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usmc ptmc": {
|
||||
"name": "USMC Pacific Missile Test Center, 1978",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usmc vmat-102": {
|
||||
"name": "USMC VMAT-102, 1980",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usn bare metal 1956": {
|
||||
"name": "USN Bare Metal, 1956",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usn nfws gray": {
|
||||
"name": "USN Naval Fighter Weapons School (Gray), 1980",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usn nfws green": {
|
||||
"name": "USN Naval Fighter Weapons School (Green), 1980",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usn vc-1 flecompron one": {
|
||||
"name": "USN VC-1 FLECOMPRON One, 1974",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usn vc-10 challengers": {
|
||||
"name": "USN VC-10 Challengers, 1988",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usn vc-5 checkertails": {
|
||||
"name": "USN VC-5 Checkertails, 1991",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usn vc-8 redtails": {
|
||||
"name": "USN VC-8 Redtails, 1985",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usn vt-7 eagles": {
|
||||
"name": "USN VT-7 Eagles, 1972",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"trainer usn vx-5 vampires": {
|
||||
"name": "USN VX-5 Vampires, 1986 (Fictional)",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"unmarked": {
|
||||
"name": "0 Unmarked",
|
||||
"countries": "All"
|
||||
},
|
||||
"usmc vma-121 green knights": {
|
||||
"name": "USMC VMA-121 Green Knights, 1968",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usmc vma-124 memphis marines": {
|
||||
"name": "USMC VMA-124 Memphis Marines, 1973",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usmc vma-131 diamondbacks": {
|
||||
"name": "USMC VMA-131 Diamondbacks, 1976",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usmc vma-142 flying gators": {
|
||||
"name": "USMC VMA-142 Flying Gators, 1973",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usmc vma-211 avengers": {
|
||||
"name": "USMC VMA-211 Avengers, 1972",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usmc vma-311 tomcats": {
|
||||
"name": "USMC VMA-311 Tomcats, 1972",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usmc vma-322 fighting gamecocks": {
|
||||
"name": "USMC VMA-322 Fighting Gamecocks, 1972",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-106 gladiators": {
|
||||
"name": "USN VA-106 Gladiators, 1967",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-144 roadrunners": {
|
||||
"name": "USN VA-144 Roadrunners, 1970",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-153 blue tail flies": {
|
||||
"name": "USN VA-153 Blue Tail Flies, 1972",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-163 saints": {
|
||||
"name": "USN VA-163 Saints, 1969",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-164 ghostriders": {
|
||||
"name": "USN VA-164 Ghostriders LADY JESSIE, 1988",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-195 dambusters": {
|
||||
"name": "USN VA-195 Dambusters, 1972",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-212 rampant raiders": {
|
||||
"name": "USN VA-212 Rampant Raiders, 1972",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-45 blackbirds": {
|
||||
"name": "USN VA-45 Blackbirds, 1972",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-46 clansmen": {
|
||||
"name": "USN VA-46 Clansmen, 1967",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-55 warhorses": {
|
||||
"name": "USN VA-55 Warhorses, 1972",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-64 black lancers": {
|
||||
"name": "USN VA-64 Black Lancers, 1972",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn va-83 rampagers": {
|
||||
"name": "USN VA-83 Rampagers, 1966",
|
||||
"countries": ["USA"]
|
||||
},
|
||||
"usn vc-7 tallyhoers": {
|
||||
"name": "USN VC-7 TallyHOers, 1972",
|
||||
"countries": ["USA"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
296
docs/mods.lua
Normal file
296
docs/mods.lua
Normal file
@@ -0,0 +1,296 @@
|
||||
-- Enter here any mods required by your mission as in the example below.
|
||||
-- Possible categories are:
|
||||
-- Aircraft
|
||||
-- Helicopter
|
||||
-- GroundUnit
|
||||
-- NavyUnit
|
||||
Olympus.modsList = {
|
||||
["A-4E-C"] = "Aircraft",
|
||||
["Bronco-OV-10A"] = "Aircraft"
|
||||
}
|
||||
|
||||
-- Enter here any unitPayloads you want to use for your mods. Remember to add the payload to the database in mods.json!
|
||||
-- DO NOT ADD PAYLOADS TO "ORIGINAL" DCS UNITS HERE! To add payloads to original DCS units, use the "unitPayload.lua" table instead and add them under the correct unit section.
|
||||
|
||||
Olympus.modsUnitPayloads = {
|
||||
["A-4E-C"] = {
|
||||
["FFAR Mk1 HE *76, Fuel 300G"] = {
|
||||
[5] = { ["CLSID"] = "{LAU3_FFAR_MK1HE}" },
|
||||
[4] = { ["CLSID"] = "{LAU3_FFAR_MK1HE}" },
|
||||
[2] = { ["CLSID"] = "{LAU3_FFAR_MK1HE}" },
|
||||
[1] = { ["CLSID"] = "{LAU3_FFAR_MK1HE}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" }
|
||||
},
|
||||
["Mk-82 SE *12"] = {
|
||||
[3] = { ["CLSID"] = "{Mk-82 Snakeye_MER_6_C}" },
|
||||
[2] = { ["CLSID"] = "{Mk-82 Snakeye_TER_2_L}" },
|
||||
[4] = { ["CLSID"] = "{Mk-82 Snakeye_TER_2_R}" },
|
||||
[1] = { ["CLSID"] = "{Mk82SNAKEYE}" },
|
||||
[5] = { ["CLSID"] = "{Mk82SNAKEYE}" }
|
||||
},
|
||||
["Mk-82 *6, Fuel 150G *2"] = {
|
||||
[3] = { ["CLSID"] = "{Mk-82_MER_6_C}" },
|
||||
[4] = { ["CLSID"] = "{DFT-150gal}" },
|
||||
[2] = { ["CLSID"] = "{DFT-150gal}" }
|
||||
},
|
||||
["Mk-82 SE *6, Fuel 150G *2"] = {
|
||||
[3] = { ["CLSID"] = "{Mk-82 Snakeye_MER_6_C}" },
|
||||
[4] = { ["CLSID"] = "{DFT-150gal}" },
|
||||
[2] = { ["CLSID"] = "{DFT-150gal}" }
|
||||
},
|
||||
["Mk-83 *3, Fuel 300G *2"] = {
|
||||
[3] = { ["CLSID"] = "{Mk-83_TER_3_C}" },
|
||||
[2] = { ["CLSID"] = "{DFT-300gal_LR}" },
|
||||
[4] = { ["CLSID"] = "{DFT-300gal_LR}" }
|
||||
},
|
||||
["Mk-84 *3"] = {
|
||||
[3] = { ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}" },
|
||||
[2] = { ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}" },
|
||||
[4] = { ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}" }
|
||||
},
|
||||
["Mk-83 *5"] = {
|
||||
[3] = { ["CLSID"] = "{Mk-83_TER_3_C}" },
|
||||
[2] = { ["CLSID"] = "{7A44FF09-527C-4B7E-B42B-3F111CFE50FB}" },
|
||||
[4] = { ["CLSID"] = "{7A44FF09-527C-4B7E-B42B-3F111CFE50FB}" }
|
||||
},
|
||||
["Mk-84 *3, Mk-82 *2"] = {
|
||||
[3] = { ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}" },
|
||||
[2] = { ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}" },
|
||||
[4] = { ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}" },
|
||||
[1] = { ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}" },
|
||||
[5] = { ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}" }
|
||||
},
|
||||
["Mk-82 SE *8, Mk-81 SE *10"] = {
|
||||
[4] = { ["CLSID"] = "{Mk-81SE_MER_5_R}" },
|
||||
[2] = { ["CLSID"] = "{Mk-81SE_MER_5_L}" },
|
||||
[3] = { ["CLSID"] = "{Mk-82 Snakeye_MER_6_C}" },
|
||||
[1] = { ["CLSID"] = "{Mk82SNAKEYE}" },
|
||||
[5] = { ["CLSID"] = "{Mk82SNAKEYE}" }
|
||||
},
|
||||
["Mk-81 *18"] = {
|
||||
[4] = { ["CLSID"] = "{Mk-81_MER_5_R}" },
|
||||
[2] = { ["CLSID"] = "{Mk-81_MER_5_L}" },
|
||||
[3] = { ["CLSID"] = "{Mk-81_MER_6_C}" },
|
||||
[1] = { ["CLSID"] = "{90321C8E-7ED1-47D4-A160-E074D5ABD902}" },
|
||||
[5] = { ["CLSID"] = "{90321C8E-7ED1-47D4-A160-E074D5ABD902}" }
|
||||
},
|
||||
["Mk-77 mod 0 *2, Mk-77 mod 1 *4"] = {
|
||||
[5] = { ["CLSID"] = "{mk77mod1}" },
|
||||
[4] = { ["CLSID"] = "{mk77mod0}" },
|
||||
[2] = { ["CLSID"] = "{mk77mod0}" },
|
||||
[1] = { ["CLSID"] = "{mk77mod1}" },
|
||||
[3] = { ["CLSID"] = "{Mk-77 mod 1_TER_2_C}" }
|
||||
},
|
||||
["Mk-82 *6, LAU-10 *4"] = {
|
||||
[3] = { ["CLSID"] = "{Mk-82_MER_6_C}" },
|
||||
[4] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[2] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[1] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[5] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" }
|
||||
},
|
||||
["Mk-4 HIPEG *2, Fuel 300G"] = {
|
||||
[3] = { ["CLSID"] = "{DFT-400gal}" },
|
||||
[2] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[4] = { ["CLSID"] = "{Mk4 HIPEG}" }
|
||||
},
|
||||
["LAU-10 *2, FFAR Mk1 HE *38, Fuel 300G"] = {
|
||||
[5] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[4] = { ["CLSID"] = "{LAU3_FFAR_MK1HE}" },
|
||||
[2] = { ["CLSID"] = "{LAU3_FFAR_MK1HE}" },
|
||||
[1] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" }
|
||||
},
|
||||
["FFAR Mk5 HEAT *76, Fuel 300G"] = {
|
||||
[5] = { ["CLSID"] = "{LAU3_FFAR_MK5HEAT}" },
|
||||
[4] = { ["CLSID"] = "{LAU3_FFAR_MK5HEAT}" },
|
||||
[2] = { ["CLSID"] = "{LAU3_FFAR_MK5HEAT}" },
|
||||
[1] = { ["CLSID"] = "{LAU3_FFAR_MK5HEAT}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" }
|
||||
},
|
||||
["AGM-45B *2, LAU-10 *2, Fuel 300G"] = {
|
||||
[2] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[4] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[5] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[1] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" }
|
||||
},
|
||||
["AGM-45B *4, Fuel 300G"] = {
|
||||
[2] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[4] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[5] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[1] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" }
|
||||
},
|
||||
["CBU-2/A *2, Fuel 300G"] = {
|
||||
[4] = { ["CLSID"] = "{CBU-2/A}" },
|
||||
[2] = { ["CLSID"] = "{CBU-2/A}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" }
|
||||
},
|
||||
["Mk-4 HIPEG *3, Mk-82SE *2"] = {
|
||||
[3] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[2] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[4] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[1] = { ["CLSID"] = "{Mk82SNAKEYE}" },
|
||||
[5] = { ["CLSID"] = "{Mk82SNAKEYE}" }
|
||||
},
|
||||
["Mk-81 SE *6, LAU-10 *2, Fuel 150G *2"] = {
|
||||
[4] = { ["CLSID"] = "{DFT-150gal}" },
|
||||
[2] = { ["CLSID"] = "{DFT-150gal}" },
|
||||
[3] = { ["CLSID"] = "{Mk-81SE_MER_6_C}" },
|
||||
[1] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[5] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" }
|
||||
},
|
||||
["AGM-45B *4"] = {
|
||||
[2] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[4] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[5] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[1] = { ["CLSID"] = "{AGM_45A}" }
|
||||
},
|
||||
["Mk-83 *5, Mk-82 *2"] = {
|
||||
[3] = { ["CLSID"] = "{Mk-83_TER_3_C}" },
|
||||
[2] = { ["CLSID"] = "{7A44FF09-527C-4B7E-B42B-3F111CFE50FB}" },
|
||||
[4] = { ["CLSID"] = "{7A44FF09-527C-4B7E-B42B-3F111CFE50FB}" },
|
||||
[5] = { ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}" },
|
||||
[1] = { ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}" }
|
||||
},
|
||||
["Mk-81 SE *18"] = {
|
||||
[4] = { ["CLSID"] = "{Mk-81SE_MER_5_R}" },
|
||||
[2] = { ["CLSID"] = "{Mk-81SE_MER_5_L}" },
|
||||
[3] = { ["CLSID"] = "{Mk-81SE_MER_6_C}" },
|
||||
[1] = { ["CLSID"] = "{MK-81SE}" },
|
||||
[5] = { ["CLSID"] = "{MK-81SE}" }
|
||||
},
|
||||
["Mk-81 SE *12, Fuel 300G"] = {
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" },
|
||||
[2] = { ["CLSID"] = "{Mk-81SE_MER_5_L}" },
|
||||
[1] = { ["CLSID"] = "{MK-81SE}" },
|
||||
[4] = { ["CLSID"] = "{Mk-81SE_MER_5_R}" },
|
||||
[5] = { ["CLSID"] = "{MK-81SE}" }
|
||||
},
|
||||
["Mk-84 *2, Fuel 300G"] = {
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" },
|
||||
[2] = { ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}" },
|
||||
[4] = { ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}" }
|
||||
},
|
||||
["CBU-2/A *2, Mk-82 SE *2, Fuel 300G"] = {
|
||||
[4] = { ["CLSID"] = "{CBU-2/A}" },
|
||||
[2] = { ["CLSID"] = "{CBU-2/A}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" },
|
||||
[5] = { ["CLSID"] = "{Mk82SNAKEYE}" },
|
||||
[1] = { ["CLSID"] = "{Mk82SNAKEYE}" }
|
||||
},
|
||||
["Mk-4 HIPEG *3, LAU-10 *2"] = {
|
||||
[3] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[2] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[4] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[1] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[5] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" }
|
||||
},
|
||||
["Mk-81 SE *10, LAU-10 *2, Fuel 300G"] = {
|
||||
[4] = { ["CLSID"] = "{Mk-81SE_MER_5_R}" },
|
||||
[2] = { ["CLSID"] = "{Mk-81SE_MER_5_L}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" },
|
||||
[1] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[5] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" }
|
||||
},
|
||||
["AGM-45B *2"] = {
|
||||
[2] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[4] = { ["CLSID"] = "{AGM_45A}" }
|
||||
},
|
||||
["GAR-8 *2, Fuel 150G"] = {
|
||||
[2] = { ["CLSID"] = "{GAR-8}" },
|
||||
[4] = { ["CLSID"] = "{GAR-8}" },
|
||||
[3] = { ["CLSID"] = "{DFT-150gal}" }
|
||||
},
|
||||
["AGM-45B *2, Fuel 300G *2"] = {
|
||||
[4] = { ["CLSID"] = "{DFT-300gal_LR}" },
|
||||
[2] = { ["CLSID"] = "{DFT-300gal_LR}" },
|
||||
[1] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[5] = { ["CLSID"] = "{AGM_45A}" }
|
||||
},
|
||||
["Mk-82 *12"] = {
|
||||
[3] = { ["CLSID"] = "{Mk-82_MER_6_C}" },
|
||||
[2] = { ["CLSID"] = "{Mk-82_TER_2_L}" },
|
||||
[4] = { ["CLSID"] = "{Mk-82_TER_2_R}" },
|
||||
[1] = { ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}" },
|
||||
[5] = { ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}" }
|
||||
},
|
||||
["CBU-2/A *2, Mk-20 *2, Fuel 300G"] = {
|
||||
[4] = { ["CLSID"] = "{CBU-2/A}" },
|
||||
[2] = { ["CLSID"] = "{CBU-2/A}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" },
|
||||
[5] = { ["CLSID"] = "{ADD3FAE1-EBF6-4EF9-8EFC-B36B5DDF1E6B}" },
|
||||
[1] = { ["CLSID"] = "{ADD3FAE1-EBF6-4EF9-8EFC-B36B5DDF1E6B}" }
|
||||
},
|
||||
["Mk-4 HIPEG *3"] = {
|
||||
[3] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[2] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[4] = { ["CLSID"] = "{Mk4 HIPEG}" }
|
||||
},
|
||||
["AGM-45B *2, LAU-10 *2"] = {
|
||||
[2] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[4] = { ["CLSID"] = "{AGM_45A}" },
|
||||
[5] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[1] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" }
|
||||
},
|
||||
["FFAR M156 WP *38, M257 Illumination *14, Fuel 300G"] = {
|
||||
[2] = { ["CLSID"] = "{LAU3_FFAR_WP156}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" },
|
||||
[4] = { ["CLSID"] = "{LAU3_FFAR_WP156}" },
|
||||
[5] = { ["CLSID"] = "{647C5F26-BDD1-41e6-A371-8DE1E4CC0E94}" },
|
||||
[1] = { ["CLSID"] = "{647C5F26-BDD1-41e6-A371-8DE1E4CC0E94}" }
|
||||
},
|
||||
["FFAR M156 WP *38, M257 Illumination *14, Mk-82 SE *6"] = {
|
||||
[2] = { ["CLSID"] = "{LAU3_FFAR_WP156}" },
|
||||
[3] = { ["CLSID"] = "{Mk-82 Snakeye_MER_6_C}" },
|
||||
[4] = { ["CLSID"] = "{LAU3_FFAR_WP156}" },
|
||||
[5] = { ["CLSID"] = "{647C5F26-BDD1-41e6-A371-8DE1E4CC0E94}" },
|
||||
[1] = { ["CLSID"] = "{647C5F26-BDD1-41e6-A371-8DE1E4CC0E94}" }
|
||||
},
|
||||
["FFAR M156 WP *38, Mk-82 SE *2, Mk-4 HIPEG"] = {
|
||||
[2] = { ["CLSID"] = "{LAU3_FFAR_WP156}" },
|
||||
[3] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[4] = { ["CLSID"] = "{LAU3_FFAR_WP156}" },
|
||||
[5] = { ["CLSID"] = "{Mk82SNAKEYE}" },
|
||||
[1] = { ["CLSID"] = "{Mk82SNAKEYE}" }
|
||||
},
|
||||
["Mk-4 HIPEG *2, FFAR M156 WP *19, LAU-10, M257 Illumination *7"] = {
|
||||
[2] = { ["CLSID"] = "{LAU3_FFAR_WP156}" },
|
||||
[3] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[4] = { ["CLSID"] = "{Mk4 HIPEG}" },
|
||||
[5] = { ["CLSID"] = "{647C5F26-BDD1-41e6-A371-8DE1E4CC0E94}" },
|
||||
[1] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" }
|
||||
},
|
||||
["Fuel 300G *3 (Ferry)"] = {
|
||||
[4] = { ["CLSID"] = "{DFT-300gal_LR}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" },
|
||||
[2] = { ["CLSID"] = "{DFT-300gal_LR}" }
|
||||
},
|
||||
["CBU-2/A *2, LAU-10 *2, Fuel 300G"] = {
|
||||
[4] = { ["CLSID"] = "{CBU-2/A}" },
|
||||
[2] = { ["CLSID"] = "{CBU-2/A}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" },
|
||||
[5] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" },
|
||||
[1] = { ["CLSID"] = "{F3EFE0AB-E91A-42D8-9CA2-B63C91ED570A}" }
|
||||
},
|
||||
["Mk-82 *8, Fuel 150G *2"] = {
|
||||
[3] = { ["CLSID"] = "{Mk-82_MER_6_C}" },
|
||||
[4] = { ["CLSID"] = "{DFT-150gal}" },
|
||||
[2] = { ["CLSID"] = "{DFT-150gal}" },
|
||||
[5] = { ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}" },
|
||||
[1] = { ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}" }
|
||||
},
|
||||
["Mk-82 SE *8, Fuel 150G *2"] = {
|
||||
[3] = { ["CLSID"] = "{Mk-82 Snakeye_MER_6_C}" },
|
||||
[4] = { ["CLSID"] = "{DFT-150gal}" },
|
||||
[2] = { ["CLSID"] = "{DFT-150gal}" },
|
||||
[5] = { ["CLSID"] = "{Mk82SNAKEYE}" },
|
||||
[1] = { ["CLSID"] = "{Mk82SNAKEYE}" }
|
||||
},
|
||||
["GAR-8 *2, Fuel 300G"] = {
|
||||
[2] = { ["CLSID"] = "{GAR-8}" },
|
||||
[4] = { ["CLSID"] = "{GAR-8}" },
|
||||
[3] = { ["CLSID"] = "{DFT-300gal}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
265
docs/old_wiki.md
Normal file
265
docs/old_wiki.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Important notice
|
||||
|
||||
The latest DCS update (DCS 2.9.18.12722) introduces a new "permission" system for mods and mission scripts. You can find more info [here](https://forum.dcs.world/topic/376636-changes-to-the-behaviour-of-netdostring_in/ )
|
||||
This will improve security and modularity but by default blocks Olympus and many other similar mods.
|
||||
|
||||
To reallow Olympus, follow these steps for every DCS instance you have installed on your system:
|
||||
|
||||
1) create a text file named Saved Games\DCS\Config\autoexec.cfg. The file extension .cfg is very important, make sure it is correctly set;
|
||||
2) using Notepad or any similar text editor, write this content into autoexec.cfg and save:
|
||||
|
||||
```
|
||||
if not net then net = {} end
|
||||
net.allow_unsafe_api = { -- this defines the secure zones where net.dostring_in() can be called from
|
||||
"userhooks",
|
||||
}
|
||||
net.allow_dostring_in = { -- and this defines the zones that should be addressed from net.dostring_in()
|
||||
"server",
|
||||
}
|
||||
```
|
||||
|
||||
That's all! Other mods/scripts may require you to further edit this file. Please refer to their specific instructions, and make sure to only enable the minimum set of permissions required by the mods you use.
|
||||
|
||||
|
||||
# Welcome to DCS Olympus' Wiki!
|
||||
|
||||
Here you will find all up-to-date information about DCS Olympus.
|
||||
|
||||
<img src="https://github.com/Pax1601/DCSOlympus/assets/7738284/0744a6d3-ff01-4e3c-a2e2-8a92f45913fe" align="right" height="240"/>
|
||||
|
||||
### User guides
|
||||
* [User guide](https://github.com/Pax1601/DCSOlympus/wiki/2.-User-Guide)
|
||||
* [New user FAQs](https://github.com/Pax1601/DCSOlympus/wiki/3.-New-User-FAQs)
|
||||
|
||||
### Join in the discussion
|
||||
|
||||
<img align="left" width="30" src="https://github.com/Pax1601/DCSOlympus/assets/103559271/0ecff279-a87c-4e2d-a4c7-da98c74adf38" />
|
||||
|
||||
[**Join our Discord**](https://discord.gg/kNAQkhUHnQ)
|
||||
|
||||
# Quick install instructions
|
||||
|
||||
To learn how to install DCS Olympus please **carefully** read the instructions [here](https://github.com/Pax1601/DCSOlympus/wiki#installation-guide).
|
||||
|
||||
The **mandatory** steps are summarized as follows:
|
||||
|
||||
1. **Completely uninstall v1.0.3**, depending on your original installation method as described [in section 1.2](https://github.com/Pax1601/DCSOlympus/wiki#12-removing-v103);
|
||||
2. **Remove your url reservation** using the `netsh delete` command as described [in paragraph 1.2.3](https://github.com/Pax1601/DCSOlympus/wiki#123-removing-the-net-shell-netsh-rule);
|
||||
3. Download the manager version of DCS Olympus from here [DCSOlympus_v1.0.4_manager.zip](https://github.com/Pax1601/DCSOlympus/releases/download/v1.0.4/DCSOlympus_v1.0.4_manager.zip);
|
||||
4. Unzip the file from the step above in your **Documents** folder;
|
||||
5. In the folder you just created, execute the **installer.bat** file by double-clicking on it;
|
||||
6. Follow the manager instructions as described [in section 2.4](https://github.com/Pax1601/DCSOlympus/wiki#24-run-the-olympus-manager).
|
||||
7. Enjoy!
|
||||
|
||||
# Installation guide
|
||||
|
||||
A full video of this process [is available on YouTube](https://www.youtube.com/watch?v=5y7ZgRrO6Fs).
|
||||
|
||||
**If you have already installed v1.0.3, you must uninstall it first - see 1.0**, and *then* install 1.0.4.
|
||||
|
||||
If you’ve never installed Olympus before, then you can go to [2.0].
|
||||
|
||||
|
||||
## [1.0] Uninstalling Olympus v1.0.3
|
||||
|
||||
### [1.1] Checking your units' databases
|
||||
If you didn't change anything in your units databases then don't worry there is nothing to do in this step, skip ahead to the next section.
|
||||
|
||||
If you did make changes to the Olympus database, like adding units or mods you’ll want to make a copy of these so you don’t lose them.
|
||||
|
||||
For most people they live in your `Saved Games\DCS.openbeta\Mods\Services\Olympus\client\public\databases` folder
|
||||
|
||||
If you went really crazy and fully added some new planes, weapons and weapons loadouts make copies of any files you changed as well before we uninstall everything.
|
||||
|
||||
### [1.2] Removing v1.0.3
|
||||
#### [1.2.1] Uninstallation method
|
||||
There were two ways you could have installed v1.0.3.
|
||||
|
||||
A. If you installed via an installer, go to [1.2.2] Case A: Uninstalling 1.0.3 if you installed via an installer;
|
||||
|
||||
B. If you manually unzipped the files into the correct locations, go to [1.2.3] Case B Uninstalling 1.0.3 if you installed via a zip.
|
||||
|
||||
##### [1.2.2] Case A: Uninstalling 1.0.3 if you installed via an installer
|
||||
|
||||
You’ll need to remove it via Windows ‘installed apps’.
|
||||
|
||||
1. Open the Start Menu.
|
||||
2. Click Settings, which looks like a cogwheel.
|
||||
3. Click Apps.
|
||||
4. Select Apps & features from the left-hand side menu.
|
||||
5. Find the Olympus program in the list that appears.
|
||||
6. Click the uninstall button that shows under the selected program or app.
|
||||
|
||||
If you didn’t see Olympus in your list and you were expecting it to, check very carefully before proceeding and/or consider if you maybe installed via the zip and forgot.
|
||||
|
||||
Once uninstalled go to [1.2.4] Removing the DCS Hooks and Scripts.
|
||||
|
||||
##### [1.2.3] Case B Uninstalling 1.0.3 if you installed via a zip
|
||||
|
||||
Olympus needs to be removed from your DCS ‘Saved Games’ folder which most commonly is found in:
|
||||
|
||||
`\Users\[user]\Saved Games\DCS.openbeta\Mods\Services\Olympus`
|
||||
|
||||
1. Remove this Olympus folder and its contents.
|
||||
|
||||
**Note:** The folder name and location can vary. It needs to be correct for your system. If you installed to a custom folder location, go there instead.
|
||||
|
||||
2. Check your `\Users\[user]\Saved Games\` folder.
|
||||
3. Remove any duplicate folders related to Olympus if they exist here.
|
||||
|
||||
Once uninstalled, go to [1.2.4] Removing the DCS Hooks and Scripts
|
||||
|
||||
**Note:** If you installed Olympus on multiple DCS instances you will obviously have to do this step for each instance.
|
||||
|
||||
### [1.2.4] Removing the DCS Hooks and Scripts
|
||||
In case something went wrong during the uninstall process, we need to remove the Olympus Hook. To do this, navigate to:
|
||||
|
||||
`\Users\[user]\Saved Games\DCS.openbeta\Scripts\Hooks\`
|
||||
|
||||
You *may* have a file called ‘OlympusHook.lua’, delete this file and *only this file*.
|
||||
|
||||
Once done go to [1.2.3] Removing the net shell (netsh) rule.
|
||||
|
||||
### [1.2.3] Removing the net shell (netsh) rule
|
||||
People who setup Olympus to be accessible from other machines - e.g. people using Olympus on a server, or PC acting like a server - will probably have setup a `netsh` rule. We’ve removed the requirement for this in v1.0.4, so let's tidy this up to allow Olympus to work properly and for security.
|
||||
|
||||
The best way to check is to open up ‘command prompt’ and enter:
|
||||
|
||||
`netsh http show urlacl`
|
||||
|
||||
You are going to see a lot of results. You are looking for things ‘Olympus related’, e.g. you may have a rule, or similar rules, which say:
|
||||
|
||||
`http://*:3001/olympus user=OlympusUser`
|
||||
|
||||
where **3001** may have a different value and **OlympusUser** will be your user.
|
||||
|
||||
If you see any, you should remove them using the following command:
|
||||
|
||||
`netsh http delete urlacl url="http://*:3001/olympus/"`
|
||||
|
||||
Take care to ensure you replace the relevant text with what it should say for your entry.
|
||||
|
||||
If you got this far, you have successfully removed Olympus v1.0.3 from your system.
|
||||
|
||||
## [2.0] Installing Olympus v1.0.4
|
||||
**If you have jumped ahead and you haven’t already uninstalled Olympus 1.0.3, you must go back and remove it first. Do not skip this step, otherwise... Well you’ve been warned.**
|
||||
|
||||
There is also a video showing you this same information which is probably a lot easier to follow, [youtube link] though this guide has a little extra information within it for more niche setups.
|
||||
|
||||
### [2.1] Downloading the manager
|
||||
|
||||
Go to the Github page and take a look at the [releases page](https://github.com/Pax1601/DCSOlympus/releases) and download the ‘manager’ version if you want to install Olympus on either your own machine or as a server.
|
||||
|
||||
Ignore the archive version, which is for a tiny, tiny, subset of people who automatically deploy Olympus - who shouldn’t need instructions to know what to do with it, so there aren’t any.
|
||||
|
||||
### [2.2] Extracting the manager
|
||||
|
||||
**DO NOT extract the download into your DCS Saved games folder or your actual DCS install**, that isn’t where it should go.
|
||||
|
||||
Find a suitable location for the file on your computer. We recommend making a folder inside your Downloads folder, but it can be extracted anywhere really.
|
||||
|
||||
Once you’ve done that, take a look at the extracted files and locate the `installer.bat` script. Note, there is another filed called `install` inside the Manager folders, ignore it.
|
||||
|
||||
### [2.3] The installer script
|
||||
|
||||
Run the script by double clicking on it and follow the relevant prompts. *Read, don't just click blindly.* You might be keen to see this new version but you may ruin things for yourself by skipping ahead too quickly.
|
||||
|
||||
The install will download a few prerequisites automatically, such as Node and the Olympus application. **This might take some time**, depending on your internet connection and whether you already had any of the prerequisites. Don’t worry if you didn’t have all the pre-requisites as the installer script will get them for you - it just takes a little longer.
|
||||
|
||||
### [2.4] Run the Olympus Manager
|
||||
|
||||
#### [2.4.1] How the manager is set up
|
||||
|
||||
Before we get to the manager, it is useful to understand how Olympus is set up.
|
||||
|
||||
Each computer with DCS installed on it has an *instance* of DCS. Most users will have only one instance, which is the one they load when running DCS. Some server owners run two DCS servers from one machine: e.g. they have "DCS Server 1" and "DCS Server 2" but these are installed in two different folders on that same machine.
|
||||
|
||||
DCS Olympus now works by having the central manager application, which manages each instance of DCS. So, in the case where a server owner runs two servers on one machine, they will have two instances in their manager, allowing them to do things like set ports and passwords independently on each instance. Again, most people will have only one instance and this is expected.
|
||||
|
||||
There are also some core important changes to how Olympus works now, so you shouldn’t need to ever run DCS as an admin for Olympus to work or do local firewall exceptions to use it on just your PC.
|
||||
|
||||
To install an ‘Olympus instance’, either run the shortcut on your desktop or find it and run it from within the relevant Olympus folder you made earlier.
|
||||
|
||||
Once that opens up, you’ll see a bunch of useful things, like User and Troubleshooting guides.
|
||||
|
||||
### [2.4.2] Basic or Expert Mode
|
||||
|
||||
We call the default install mode ‘Basic mode’ but there is nothing basic about it - it just means it’s ‘easy’ to use. In fact, it’s what we use to install Olympus ourselves. The eagle-eyed will have noticed there is also an ‘Expert mode’ for those who need to fettle more. This isn’t a challenge or a fun hard game mode, it just doesn’t guide you through things logically and makes it easier to pick the wrong settings. Expert mode is primarily for managing complicated setups.
|
||||
|
||||
So let's keep going with basic mode. Click ‘Add Olympus’.
|
||||
|
||||
### [2.4.3] Detecting DCS Saved Games Folders
|
||||
|
||||
The manager will detect where your DCS Saved Games folders are and give you the option to install to them. Naturally, you should pick the folder into which you want to install it. Most people likely have just one folder and not be given a choice. If you do later try install again over this folder, it’s like a factory reset of the install.
|
||||
|
||||
### [2.4.4] Singleplayer Olympus or multiplayer
|
||||
|
||||
All this really means is do you want to only use and control Olympus from this computer, if you do pick Singleplayer.
|
||||
|
||||
If you want others to be able to control this instance of Olympus from other computers - including other devices - pick Multiplayer.
|
||||
|
||||
You can always reinstall again if you change your mind.
|
||||
|
||||
### [2.4.5] Set port and address settings
|
||||
|
||||
We can now set these port settings for you, or if you want to pick different defaults, you can do that instead.
|
||||
|
||||
We recommend you pick ‘Auto apply settings’ and click Next.
|
||||
|
||||
However, by choosing Manually set the next screen will give you options to pick manually a frontend or backend port.
|
||||
|
||||
### [2.4.6] Manually setting Olympus port and address settings
|
||||
|
||||
**If you picked ‘Auto apply settings’, go to [2.4.7] Pick some passwords.**
|
||||
|
||||
You will be prompted to pick ports for the front and backend port. The green tick indicates this port is *probably* free for use and not taken by another running process or Olympus instance. Note: you might have to click on the screen to see this update.
|
||||
|
||||
However, if a port isn't in use because the application which uses it isn't running currently, it may cause issues when it does. If you have any problems, just come back here and pick a free port.
|
||||
|
||||
**Leave the option unchecked** for ‘Enable direct backend API connection’ it’s no longer needed for Olympus unless you are doing some real niche custom modding things, we’ll probably tell you in Discord if you should enable this otherwise leave it unchecked.
|
||||
|
||||
### [2.4.7] Pick some passwords
|
||||
|
||||
This step is so obvious, I don’t think we are going to tell you how to do this.
|
||||
|
||||
Remember to pick secure, long, passwords if you will be using this over the internet and not just on your PC. [Obligatory XKCD](https://xkcd.com/936/)
|
||||
|
||||
All passwords must also be different within each instance.
|
||||
|
||||
If you have Olympus already installed, with chosen passwords, you can click Next to keep the same passwords.
|
||||
|
||||
### [2.4.8] Install the new camera control plugin
|
||||
|
||||
We’ve got a new camera control plugin which lets you move the DCS camera around in missions to wherever you are looking within DCS, which is really neat.
|
||||
|
||||
Obviously, we recommend you install this because it’s cool but here you get the option if you don’t want to.
|
||||
|
||||
Importantly, **if you want to use the camera control functionality**, even with other people’s Olympus servers or machines, **you’ll need to install the plugin for yourself** by saying install here as it connects to your local DCS. This isn’t a server-side thing.
|
||||
|
||||
For more information on the camera plugin, including troubleshooting and removal of the plugin, see [2.0] Live Camera Plugin on [4. Setup FAQ and Troubleshooting](https://github.com/Pax1601/DCSOlympus/wiki/4.-Setup-FAQ-and-Troubleshooting).
|
||||
|
||||
### [2.4.9] Allow GitHub’s Electron
|
||||
|
||||
You *might* get a prompt to Allow Electron to run - but not everyone will. It’s something we use to build Olympus. Electron is made by GitHub, which you can have a read about on the internet.
|
||||
|
||||
If you see the prompt, click ‘Allow’ and you will finish your Olympus install.
|
||||
|
||||
## [3.0] Enjoy Olympus
|
||||
|
||||
### [3.1] Getting a mission running
|
||||
|
||||
Okay, hopefully that worked for you and you can launch Olympus. Follow the instructions on screen. You don’t need the manager open to run Olympus with DCS.
|
||||
|
||||
Remember: to load up a mission and **remember to unpause the game/server** or Olympus can’t do anything!
|
||||
|
||||
### [3.2] Change settings / uninstall Olympus
|
||||
|
||||
Once you’ve installed an Olympus instance, you’ll see it listed here. Clicking ‘Edit Settings’ will walk you back through the wizard again to amend the relevant settings. You can also see ‘Open logs’ that will open the two logs people need to help troubleshoot issues.
|
||||
|
||||
It may appear that only one log window has been opened, but sometimes one log window sits over the other, so move the one you can see in case it's hiding the second.
|
||||
|
||||
You can also uninstall that specific Olympus instance as well. This doesn’t remove every instance of Olympus from your machine, just that specific one. If you want to remove them all you’ll have to repeat this process for each installed instance.
|
||||
|
||||
## [4.0] Troubleshooting and further steps
|
||||
|
||||
If you do have problems, or want to change any settings, you can ‘Return to the main window’ and instead click ‘Change settings’. This is also how you find the super important logs you must share with us if you want help from people in our [DCS Olympus Development Discord server](https://discord.gg/tuDd94xC4A). See the #troubleshooting channel.
|
||||
@@ -1,25 +1,3 @@
|
||||
# Contents
|
||||
See GitHub Contents on the right of the page.
|
||||
<!--- [Introduction](#introduction)
|
||||
- [What is Olympus?](#what-is-olympus)
|
||||
- [The Team](#the-team)
|
||||
- [Basics](#basics)
|
||||
- [Installing Olympus](#installing-olympus)
|
||||
- [Running Olympus](#running-olympus)
|
||||
- [User Interface](#user-interface)
|
||||
- [Overview](#overview)
|
||||
- [Map Options](#map-options)
|
||||
- [Show/hide options](#showhide-options)
|
||||
- [Game Master Options](#game-master-options)
|
||||
- [Airfields](#airfields)
|
||||
- [Cursor Location Info](#cursor-location-info)
|
||||
- [Connection Status](#connection-status)
|
||||
- [Selected unit Information](#selected-unit-information)
|
||||
- [Selected Units Window](#selected-units-window)
|
||||
- [Selected Unit](#selected-unit)
|
||||
- [Using the Map](#using-the-map)
|
||||
- [Spawning Units](#spawning-units)-->
|
||||
|
||||
# Introduction
|
||||
|
||||
## What is Olympus?
|
||||
@@ -45,86 +23,13 @@ The user interface is key to this experience, and we have done our best to make
|
||||
|
||||
# Basics
|
||||
|
||||
## Installing Olympus
|
||||
|
||||
**NOTE: the installation process has been greatly modified in version v1.0.4. These instructions are NOT applicable to v1.0.3. Users are suggested to update to the latest version.**
|
||||
|
||||
**NOTE: Olympus is offered in two different packages: an autoinstaller package and an archive package. The autoinstaller package is marked with the prefix ```autoinstaller_```. THESE INSTRUCTION ARE ONLY APPLICABLE TO THE AUTOINSTALLER VERSION. The archived version is intended for automatic tools like [SpecialK's Server Bot](https://github.com/Special-K-s-Flightsim-Bots/DCSServerBot).**
|
||||
|
||||
Download the latest release autoinstaller package from the [Github releases page](https://github.com/Pax1601/DCSOlympus/releases). After downloading the package, unpack it and follow these steps:
|
||||
|
||||
1) **Close any applications which may interfere with installation**, including Digital Combat Simulator (DCS) and previous versions of Olympus.
|
||||
<br>
|
||||
2) **If you DO NOT have Olympus v1.0.3 already installed, SKIP THIS STEP. If you have already installed Olympus v1.0.3, do the following**:
|
||||
NOTE: If you made any changes to your unit databases or mods.lua file (e.g. to support a third party mod) make a backup of the edited files before proceeding or changes will be lost;
|
||||
a) **If you installed DCS Olympus v1.0.3 using the installer**, simply remove it using Windows's "Add or remove programs" application.
|
||||
b) **If you installed DCS Olympus v1.0.3 using the archived version**, remove it by deleting the "...<DCS Saved Games folder>\Mods\Services\Olympus" folder. Do this for every DCS instance you installed Olympus in.
|
||||
Remember to delete any shortcuts you created. Don't worry, they will be created automatically again by the installation script provided in this package.
|
||||
<br>
|
||||
3) **Create a folder named "DCS Olympus"** in your "Saved Games" directory and **extract all the contents of the downloaded package into it**.
|
||||
NOTE:
|
||||
a) Do not extract the contents of the package directly in your Saved Games folder or in your DCS Saved Games folder.
|
||||
b) Unlike previous version of Olympus, it is no longer necessary to copy the packaged files into each DCS instance folder.
|
||||
<br>
|
||||
4) **Execute the "installer.bat"** script by double-clicking on it. It is located in the folder you created in step 3. Wait for the installation script to complete. **Installation may take a couple of minutes, after which the Manager will start automatically**.
|
||||
NOTE: depending on your Windows configuration, the script may be called "installer" (without .bat at the end).
|
||||
<br>
|
||||
5) The Olympus Manager will open. This will allow you to add/remove Olympus to individual DCS instances.
|
||||
**Use the Olympus Manager and follow the instructions to install and setup Olympus** (see the dedicated section below for more information).
|
||||
<br>
|
||||
6) **Start DCS and run a mission. Make sure it is UNPAUSED**.
|
||||
<br>
|
||||
7) **Open Olympus via the shortcut and login using any username and the Game Master password set using the Manager. (NOTE: not your DCS server password)**.
|
||||
Local installation: run the client from the provided desktop shortcut.
|
||||
Dedicated server: users must first start the Olympus server from the provided desktop shortcut or using the "Expert view" mode of the Manager.
|
||||
Then log in using any browser and visiting ```http:\\server_IP:frontend_port``` (frontend port is 3000 by default, but can be edited using the Manager)
|
||||
<br>
|
||||
8) You can use the manager at any time to change the ports and/or passwords. If you do, **REMEMBER TO RESTART OLYMPUS AND DCS**.
|
||||
|
||||
NOTES:
|
||||
a) when launching the Manager you will be prompted to allow Electron to create a firewall rule. This is optional and can be denied without effect on the operation of the Manager;
|
||||
b) if you are using Olympus on a dedicated server with a router, you must enable port forwarding on the frontend port (3000 by default);
|
||||
c) unlike Olympus v1.0.3, **running the netsh command is no longer required. It is also no longer required to create firewall rules or port forwarding for the backend port. (Optional) If you already performed this steps in the past you can delete the firewall and netsh rules**.
|
||||
|
||||
|
||||
Once you have installed Olympus, it will run whenever you are playing a DCS local mission or hosting a multiplayer server. To control the action, open the Olympus client via the icon on your desktop or start menu.
|
||||
|
||||
## Using the Manager
|
||||
The Olympus Manager is used to install, update, and configure your Olympus installation. If you own a dedicated server and run multiple Olympus instances at the same time, the Manager allows you to conveniently handle them from a single location.
|
||||
|
||||
**REMEMBER, after installing Olympus with step 4) of the installation process above, you will still need to add it to your DCS instance!**
|
||||
|
||||
When you start the Manager for the first time, it will allow to choose between Basic and Expert mode. Basic mode is suggested for local only users, while Expert mode is tailored for dedicated server owners. You can change the mode at any time with the link in the toolbar at the top of the manager. Note that the choice of the Manager mode has no impact on the functionality of Olympus. Server owners can use Basic mode, or vice versa.
|
||||
|
||||
### Basic mode
|
||||
|
||||
Basic mode gives you access to an installation Wizard. To start it, click on "Add Olympus". If you have more than one DCS instance installed, the Wizard will allow you to select the one you want to add Olympus to. If only one instance is detected, this step will be skipped.
|
||||
|
||||
**Should the Wizard fail at detecting your DCS instance, you can manually add it. To do so, open the ```<Olympus installation folder>\manager\options.json``` file and add the location of your DCS instance, between double quotes, in ```additionalDCSInstances```. Multiple values should be separated by a comma. Use double backslashes as separators**
|
||||
|
||||
Once the DCS instance is selected, you will be prompted with a series of questions which will allow you configure your installation. The meaning of all the questions and the options is explained in the Manager itself, move your mouse cursor on the info icons for more information.
|
||||
|
||||
### Expert mode
|
||||
|
||||
Expert mode operates more as a dashboard rather than a Wizard. All DCS instances are presented to the user, and you will be able to install/configure/remove Olympus from a single page.
|
||||
|
||||
The meaning of all the options is also explained by moving your mouse cursor on the info icon.
|
||||
|
||||
In Expert mode, users are also capable of starting the Olympus client and server directly from the Manager. This is convenient if you have multiple installations. Once a client/server instance is running, the Manager allows you to monitor its state directly from the Expert view.
|
||||
|
||||
## Updating Olympus
|
||||
|
||||
The Manager allows you to update Olympus automatically. When the Manager is started, it checks if a new version is available. If that is the case, you will be given the option to automatically update Olympus. The new package will be downloaded and installed without requiring any inputs from the user.
|
||||
|
||||
The Manager will be closed and reopened after the process is completed. Once this is done, you will presented with a warning: just like installing Olympus will not automatically add it to your DCS instance, updating it will not update it in your DCS instance. By clicking on "Accept", all your Olympus instances will be automatically updated.
|
||||
|
||||
## Logging into Olympus
|
||||
|
||||
To login to Olympus, you need to enter a username and password.
|
||||
|
||||
**The username field is used for logging purposes only and can be anything you choose. The password is the one you set during the installation.**
|
||||
|
||||
If you forget a password or need to change them for security purposes, use the manager to configure it again, then restart the Olympus client\server and DCS mission.
|
||||
If you forget a password or need to change them for security purposes, use ```configurator.exe``` in ```DCS.openbeta/Mods/Services/Olympus```, then restart the Olympus client and DCS mission.
|
||||
|
||||
Please note that at the moment, ```configurator.exe``` is a feature which is not present in local installations created via the installer, but is present when Olympus is installed via any of the other methods. In order to reset the Olympus client passwords for local installations, you will need to reinstall Olympus. This will be addressed in a future update.
|
||||
|
||||
|
||||
6
frontend/react/.vscode/tasks.json
vendored
6
frontend/react/.vscode/tasks.json
vendored
@@ -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",
|
||||
|
||||
@@ -268,6 +268,14 @@ export const minimapBoundaries = {
|
||||
new LatLng(54.724620, 16.413574),
|
||||
new LatLng(54.724620, 5.570068),
|
||||
],
|
||||
Iraq: [
|
||||
// Iraq
|
||||
new LatLng(36.672825, 39.413452),
|
||||
new LatLng(29.798943, 39.413452),
|
||||
new LatLng(29.798943, 53.201294),
|
||||
new LatLng(36.672825, 53.201294),
|
||||
new LatLng(36.672825, 39.413452),
|
||||
]
|
||||
};
|
||||
|
||||
export const mapBounds = {
|
||||
@@ -305,7 +313,8 @@ export const mapBounds = {
|
||||
zoom: 4,
|
||||
},
|
||||
Afghanistan: { bounds: new LatLngBounds([36.22, 61.21], [30.42, 68.05]), zoom: 5 },
|
||||
GermanyCW: { bounds: new LatLngBounds([54.724620, 5.570068], [49.282140, 16.413574]), zoom: 4 }
|
||||
GermanyCW: { bounds: new LatLngBounds([54.724620, 5.570068], [49.282140, 16.413574]), zoom: 4 },
|
||||
Iraq: { bounds: new LatLngBounds([36.672825, 39.413452], [29.798943, 53.201294]), zoom: 4 },
|
||||
};
|
||||
|
||||
export const defaultMapMirrors = {};
|
||||
@@ -351,6 +360,7 @@ export enum OlympusState {
|
||||
MEASURE = "Measure",
|
||||
TRAINING = "Training",
|
||||
ADMIN = "Admin",
|
||||
IMPORT_IMAGE_OVERLAY = "Import image overlay"
|
||||
}
|
||||
|
||||
export const NO_SUBSTATE = "No substate";
|
||||
@@ -389,6 +399,7 @@ export enum SpawnSubState {
|
||||
NO_SUBSTATE = "No substate",
|
||||
SPAWN_UNIT = "Unit",
|
||||
SPAWN_EFFECT = "Effect",
|
||||
LOADOUT_WIZARD = "Loadout wizard"
|
||||
}
|
||||
|
||||
export enum OptionsSubstate {
|
||||
@@ -538,6 +549,10 @@ export enum DataIndexes {
|
||||
aimMethodRange,
|
||||
acquisitionRange,
|
||||
airborne,
|
||||
cargoWeight,
|
||||
drawingArguments,
|
||||
customString,
|
||||
customInteger,
|
||||
endOfData = 255,
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,392 +2,405 @@ import { LatLng } from "leaflet";
|
||||
import { AudioOptions, Coalition, MapOptions } from "./types/types";
|
||||
|
||||
export interface OlympusConfig {
|
||||
/* Set by user */
|
||||
frontend: {
|
||||
port: number;
|
||||
elevationProvider: {
|
||||
provider: string;
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
};
|
||||
mapLayers: {
|
||||
[key: string]: {
|
||||
urlTemplate: string;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
attribution?: string;
|
||||
};
|
||||
};
|
||||
mapMirrors: {
|
||||
[key: string]: string;
|
||||
/* Set by user */
|
||||
frontend: {
|
||||
port: number;
|
||||
elevationProvider: {
|
||||
provider: string;
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
};
|
||||
mapLayers: {
|
||||
[key: string]: {
|
||||
urlTemplate: string;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
attribution?: string;
|
||||
};
|
||||
};
|
||||
mapMirrors: {
|
||||
[key: string]: string;
|
||||
};
|
||||
/* New with v2.0.0 */
|
||||
customAuthHeaders?: {
|
||||
enabled: boolean;
|
||||
username: string;
|
||||
group: string;
|
||||
};
|
||||
autoconnectWhenLocal?: boolean;
|
||||
};
|
||||
/* New with v2.0.0 */
|
||||
customAuthHeaders?: {
|
||||
enabled: boolean;
|
||||
username: string;
|
||||
group: string;
|
||||
audio?: {
|
||||
SRSPort: number;
|
||||
WSPort?: number;
|
||||
WSEndpoint?: string;
|
||||
};
|
||||
autoconnectWhenLocal?: boolean;
|
||||
};
|
||||
/* New with v2.0.0 */
|
||||
audio?: {
|
||||
SRSPort: number;
|
||||
WSPort?: number;
|
||||
WSEndpoint?: string;
|
||||
};
|
||||
controllers?: [{ type: string; coalition: Coalition; frequency: number; modulation: number; callsign: string }];
|
||||
profiles?: { [key: string]: ProfileOptions };
|
||||
controllers?: [{ type: string; coalition: Coalition; frequency: number; modulation: number; callsign: string }];
|
||||
profiles?: { [key: string]: ProfileOptions };
|
||||
|
||||
/* Set by server */
|
||||
local?: boolean;
|
||||
authentication?: {
|
||||
// Only sent when in localhost mode for autologin
|
||||
gameMasterPassword: string;
|
||||
blueCommanderPassword: string;
|
||||
redCommanderPassword: string;
|
||||
};
|
||||
/* Set by server */
|
||||
local?: boolean;
|
||||
authentication?: {
|
||||
// Only sent when in localhost mode for autologin
|
||||
gameMasterPassword: string;
|
||||
blueCommanderPassword: string;
|
||||
redCommanderPassword: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
radios?: { frequency: number; modulation: number; pan: number }[];
|
||||
fileSources?: { filename: string; volume: number }[];
|
||||
unitSinks?: { ID: number }[];
|
||||
connections?: any[];
|
||||
coalitionAreas?: (
|
||||
| { type: "circle"; label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition }
|
||||
| { type: "polygon"; label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition }
|
||||
)[];
|
||||
hotgroups?: { [key: string]: number[] };
|
||||
starredSpawns?: { [key: number]: SpawnRequestTable };
|
||||
drawings?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } };
|
||||
navpoints?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } };
|
||||
mapSource?: { id: string };
|
||||
radios?: { frequency: number; modulation: number; pan: number }[];
|
||||
fileSources?: { filename: string; volume: number }[];
|
||||
unitSinks?: { ID: number }[];
|
||||
connections?: any[];
|
||||
coalitionAreas?: (
|
||||
| { type: "circle"; label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition }
|
||||
| { type: "polygon"; label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition }
|
||||
)[];
|
||||
hotgroups?: { [key: string]: number[] };
|
||||
starredSpawns?: { [key: number]: SpawnRequestTable };
|
||||
drawings?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } };
|
||||
navpoints?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } };
|
||||
mapSource?: { id: string };
|
||||
customLoadouts?: { [key: string]: LoadoutBlueprint[] };
|
||||
}
|
||||
|
||||
export interface ProfileOptions {
|
||||
mapOptions?: MapOptions;
|
||||
shortcuts?: { [key: string]: ShortcutOptions };
|
||||
audioOptions?: AudioOptions;
|
||||
mapOptions?: MapOptions;
|
||||
shortcuts?: { [key: string]: ShortcutOptions };
|
||||
audioOptions?: AudioOptions;
|
||||
}
|
||||
|
||||
export interface ContextMenuOption {
|
||||
tooltip: string;
|
||||
src: string;
|
||||
callback: CallableFunction;
|
||||
tooltip: string;
|
||||
src: string;
|
||||
callback: CallableFunction;
|
||||
}
|
||||
|
||||
export interface AirbasesData {
|
||||
airbases: { [key: string]: any };
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
airbases: { [key: string]: any };
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface BullseyesData {
|
||||
bullseyes: {
|
||||
[key: string]: { latitude: number; longitude: number; coalition: string };
|
||||
};
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
bullseyes: {
|
||||
[key: string]: { latitude: number; longitude: number; coalition: string };
|
||||
};
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface SpotsData {
|
||||
spots: {
|
||||
[key: string]: { active: boolean; type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number };
|
||||
};
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
spots: {
|
||||
[key: string]: { active: boolean; type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number };
|
||||
};
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface MissionData {
|
||||
mission: {
|
||||
theatre: string;
|
||||
dateAndTime: DateAndTime;
|
||||
commandModeOptions: CommandModeOptions;
|
||||
coalitions: { red: string[]; blue: string[] };
|
||||
};
|
||||
time: number;
|
||||
sessionHash: string;
|
||||
mission: {
|
||||
theatre: string;
|
||||
dateAndTime: DateAndTime;
|
||||
commandModeOptions: CommandModeOptions;
|
||||
coalitions: { red: string[]; blue: string[] };
|
||||
};
|
||||
time: number;
|
||||
sessionHash: string;
|
||||
}
|
||||
|
||||
export interface CommandModeOptions {
|
||||
commandMode: string;
|
||||
restrictSpawns: boolean;
|
||||
restrictToCoalition: boolean;
|
||||
setupTime: number;
|
||||
spawnPoints: {
|
||||
red: number;
|
||||
blue: number;
|
||||
};
|
||||
eras: string[];
|
||||
commandMode: string;
|
||||
restrictSpawns: boolean;
|
||||
restrictToCoalition: boolean;
|
||||
setupTime: number;
|
||||
spawnPoints: {
|
||||
red: number;
|
||||
blue: number;
|
||||
};
|
||||
eras: string[];
|
||||
}
|
||||
|
||||
export interface DateAndTime {
|
||||
date: { Year: number; Month: number; Day: number };
|
||||
time: { h: number; m: number; s: number };
|
||||
elapsedTime: number;
|
||||
startTime: number;
|
||||
date: { Year: number; Month: number; Day: number };
|
||||
time: { h: number; m: number; s: number };
|
||||
elapsedTime: number;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
export interface LogData {
|
||||
logs: { [key: string]: string };
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
logs: { [key: string]: string };
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface ServerRequestOptions {
|
||||
time?: number;
|
||||
commandHash?: string;
|
||||
time?: number;
|
||||
commandHash?: string;
|
||||
}
|
||||
|
||||
export interface SpawnRequestTable {
|
||||
category: string;
|
||||
coalition: string;
|
||||
unit: UnitSpawnTable;
|
||||
amount: number;
|
||||
quickAccessName?: string;
|
||||
category: string;
|
||||
coalition: string;
|
||||
unit: UnitSpawnTable;
|
||||
amount: number;
|
||||
quickAccessName?: string;
|
||||
}
|
||||
|
||||
export interface EffectRequestTable {
|
||||
type: string;
|
||||
explosionType?: string;
|
||||
smokeColor?: string;
|
||||
type: string;
|
||||
explosionType?: string;
|
||||
smokeColor?: string;
|
||||
}
|
||||
|
||||
export interface UnitSpawnTable {
|
||||
unitType: string;
|
||||
location: LatLng;
|
||||
skill: string;
|
||||
liveryID: string;
|
||||
altitude?: number;
|
||||
loadout?: string;
|
||||
heading?: number;
|
||||
unitType: string;
|
||||
location: LatLng;
|
||||
skill: string;
|
||||
liveryID: string;
|
||||
altitude?: number;
|
||||
loadout?: string;
|
||||
heading?: number;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface ObjectIconOptions {
|
||||
showState: boolean;
|
||||
showVvi: boolean;
|
||||
showHealth: boolean;
|
||||
showHotgroup: boolean;
|
||||
showUnitIcon: boolean;
|
||||
showShortLabel: boolean;
|
||||
showFuel: boolean;
|
||||
showAmmo: boolean;
|
||||
showSummary: boolean;
|
||||
showCallsign: boolean;
|
||||
rotateToHeading: boolean;
|
||||
showCluster: boolean;
|
||||
showAlarmState: boolean;
|
||||
showState: boolean;
|
||||
showVvi: boolean;
|
||||
showHealth: boolean;
|
||||
showHotgroup: boolean;
|
||||
showUnitIcon: boolean;
|
||||
showShortLabel: boolean;
|
||||
showFuel: boolean;
|
||||
showAmmo: boolean;
|
||||
showSummary: boolean;
|
||||
showCallsign: boolean;
|
||||
rotateToHeading: boolean;
|
||||
showCluster: boolean;
|
||||
showAlarmState: boolean;
|
||||
}
|
||||
|
||||
export interface GeneralSettings {
|
||||
prohibitJettison: boolean;
|
||||
prohibitAA: boolean;
|
||||
prohibitAG: boolean;
|
||||
prohibitAfterburner: boolean;
|
||||
prohibitAirWpn: boolean;
|
||||
prohibitJettison: boolean;
|
||||
prohibitAA: boolean;
|
||||
prohibitAG: boolean;
|
||||
prohibitAfterburner: boolean;
|
||||
prohibitAirWpn: boolean;
|
||||
}
|
||||
|
||||
export interface TACAN {
|
||||
isOn: boolean;
|
||||
channel: number;
|
||||
XY: string;
|
||||
callsign: string;
|
||||
isOn: boolean;
|
||||
channel: number;
|
||||
XY: string;
|
||||
callsign: string;
|
||||
}
|
||||
|
||||
export interface Radio {
|
||||
frequency: number;
|
||||
callsign: number;
|
||||
callsignNumber: number;
|
||||
frequency: number;
|
||||
callsign: number;
|
||||
callsignNumber: number;
|
||||
}
|
||||
|
||||
export interface Ammo {
|
||||
quantity: number;
|
||||
name: string;
|
||||
guidance: number;
|
||||
category: number;
|
||||
missileCategory: number;
|
||||
quantity: number;
|
||||
name: string;
|
||||
guidance: number;
|
||||
category: number;
|
||||
missileCategory: number;
|
||||
}
|
||||
|
||||
export interface Contact {
|
||||
ID: number;
|
||||
detectionMethod: number;
|
||||
ID: number;
|
||||
detectionMethod: number;
|
||||
}
|
||||
|
||||
export interface Offset {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
export interface DrawingArgument {
|
||||
argument: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface UnitData {
|
||||
category: string;
|
||||
markerCategory: string;
|
||||
ID: number;
|
||||
alive: boolean;
|
||||
alarmState: AlarmState;
|
||||
human: boolean;
|
||||
controlled: boolean;
|
||||
coalition: string;
|
||||
country: number;
|
||||
name: string;
|
||||
unitName: string;
|
||||
callsign: string;
|
||||
unitID: number;
|
||||
groupID: number;
|
||||
groupName: string;
|
||||
state: string;
|
||||
task: string;
|
||||
hasTask: boolean;
|
||||
position: LatLng;
|
||||
speed: number;
|
||||
horizontalVelocity: number;
|
||||
verticalVelocity: number;
|
||||
heading: number;
|
||||
track: number;
|
||||
isActiveTanker: boolean;
|
||||
isActiveAWACS: boolean;
|
||||
onOff: boolean;
|
||||
followRoads: boolean;
|
||||
fuel: number;
|
||||
desiredSpeed: number;
|
||||
desiredSpeedType: string;
|
||||
desiredAltitude: number;
|
||||
desiredAltitudeType: string;
|
||||
leaderID: number;
|
||||
formationOffset: Offset;
|
||||
targetID: number;
|
||||
targetPosition: LatLng;
|
||||
ROE: string;
|
||||
reactionToThreat: string;
|
||||
emissionsCountermeasures: string;
|
||||
TACAN: TACAN;
|
||||
radio: Radio;
|
||||
generalSettings: GeneralSettings;
|
||||
ammo: Ammo[];
|
||||
contacts: Contact[];
|
||||
activePath: LatLng[];
|
||||
isLeader: boolean;
|
||||
operateAs: string;
|
||||
shotsScatter: number;
|
||||
shotsIntensity: number;
|
||||
health: number;
|
||||
racetrackLength: number;
|
||||
racetrackAnchor: LatLng;
|
||||
racetrackBearing: number;
|
||||
timeToNextTasking: number;
|
||||
barrelHeight: number;
|
||||
muzzleVelocity: number;
|
||||
aimTime: number;
|
||||
shotsToFire: number;
|
||||
shotsBaseInterval: number;
|
||||
shotsBaseScatter: number;
|
||||
engagementRange: number;
|
||||
targetingRange: number;
|
||||
aimMethodRange: number;
|
||||
acquisitionRange: number;
|
||||
airborne: boolean;
|
||||
category: string;
|
||||
markerCategory: string;
|
||||
ID: number;
|
||||
alive: boolean;
|
||||
alarmState: AlarmState;
|
||||
human: boolean;
|
||||
controlled: boolean;
|
||||
coalition: string;
|
||||
country: number;
|
||||
name: string;
|
||||
unitName: string;
|
||||
callsign: string;
|
||||
unitID: number;
|
||||
groupID: number;
|
||||
groupName: string;
|
||||
state: string;
|
||||
task: string;
|
||||
hasTask: boolean;
|
||||
position: LatLng;
|
||||
speed: number;
|
||||
horizontalVelocity: number;
|
||||
verticalVelocity: number;
|
||||
heading: number;
|
||||
track: number;
|
||||
isActiveTanker: boolean;
|
||||
isActiveAWACS: boolean;
|
||||
onOff: boolean;
|
||||
followRoads: boolean;
|
||||
fuel: number;
|
||||
desiredSpeed: number;
|
||||
desiredSpeedType: string;
|
||||
desiredAltitude: number;
|
||||
desiredAltitudeType: string;
|
||||
leaderID: number;
|
||||
formationOffset: Offset;
|
||||
targetID: number;
|
||||
targetPosition: LatLng;
|
||||
ROE: string;
|
||||
reactionToThreat: string;
|
||||
emissionsCountermeasures: string;
|
||||
TACAN: TACAN;
|
||||
radio: Radio;
|
||||
generalSettings: GeneralSettings;
|
||||
ammo: Ammo[];
|
||||
contacts: Contact[];
|
||||
activePath: LatLng[];
|
||||
isLeader: boolean;
|
||||
operateAs: string;
|
||||
shotsScatter: number;
|
||||
shotsIntensity: number;
|
||||
health: number;
|
||||
racetrackLength: number;
|
||||
racetrackAnchor: LatLng;
|
||||
racetrackBearing: number;
|
||||
timeToNextTasking: number;
|
||||
barrelHeight: number;
|
||||
muzzleVelocity: number;
|
||||
aimTime: number;
|
||||
shotsToFire: number;
|
||||
shotsBaseInterval: number;
|
||||
shotsBaseScatter: number;
|
||||
engagementRange: number;
|
||||
targetingRange: number;
|
||||
aimMethodRange: number;
|
||||
acquisitionRange: number;
|
||||
airborne: boolean;
|
||||
cargoWeight: number;
|
||||
drawingArguments: DrawingArgument[];
|
||||
customString: string;
|
||||
customInteger: number;
|
||||
}
|
||||
|
||||
export interface LoadoutItemBlueprint {
|
||||
name: string;
|
||||
quantity: number;
|
||||
effectiveAgainst?: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface LoadoutBlueprint {
|
||||
fuel: number;
|
||||
items: LoadoutItemBlueprint[];
|
||||
roles: string[];
|
||||
code: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
items: LoadoutItemBlueprint[];
|
||||
roles: string[];
|
||||
code: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
isCustom?: boolean;
|
||||
persistent?: boolean;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface UnitBlueprint {
|
||||
name: string;
|
||||
category: string;
|
||||
enabled: boolean;
|
||||
coalition: string;
|
||||
era: string;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
roles?: string[];
|
||||
type?: string;
|
||||
loadouts?: LoadoutBlueprint[];
|
||||
filename?: string;
|
||||
liveries?: { [key: string]: { name: string; countries: string[] } };
|
||||
cost?: number;
|
||||
barrelHeight?: number;
|
||||
muzzleVelocity?: number;
|
||||
aimTime?: number;
|
||||
shotsToFire?: number;
|
||||
shotsBaseInterval?: number;
|
||||
shotsBaseScatter?: number;
|
||||
description?: string;
|
||||
abilities?: string;
|
||||
tags?: string;
|
||||
acquisitionRange?: number;
|
||||
engagementRange?: number;
|
||||
targetingRange?: number;
|
||||
aimMethodRange?: number;
|
||||
alertnessTimeConstant?: number;
|
||||
canTargetPoint?: boolean;
|
||||
canRearm?: boolean;
|
||||
canAAA?: boolean;
|
||||
indirectFire?: boolean;
|
||||
markerFile?: string;
|
||||
unitWhenGrouped?: string;
|
||||
mainRole?: string;
|
||||
length?: number;
|
||||
carrierFilename?: string;
|
||||
name: string;
|
||||
category: string;
|
||||
enabled: boolean;
|
||||
coalition: string;
|
||||
era: string;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
roles?: string[];
|
||||
type?: string;
|
||||
loadouts?: LoadoutBlueprint[];
|
||||
acceptedPayloads?: { [key: string]: { clsid: string; name: string; weight: number }[] };
|
||||
filename?: string;
|
||||
liveries?: { [key: string]: { name: string; countries: string[] } };
|
||||
cost?: number;
|
||||
barrelHeight?: number;
|
||||
muzzleVelocity?: number;
|
||||
aimTime?: number;
|
||||
shotsToFire?: number;
|
||||
shotsBaseInterval?: number;
|
||||
shotsBaseScatter?: number;
|
||||
description?: string;
|
||||
abilities?: string;
|
||||
tags?: string;
|
||||
acquisitionRange?: number;
|
||||
engagementRange?: number;
|
||||
targetingRange?: number;
|
||||
aimMethodRange?: number;
|
||||
alertnessTimeConstant?: number;
|
||||
canTargetPoint?: boolean;
|
||||
canRearm?: boolean;
|
||||
canAAA?: boolean;
|
||||
indirectFire?: boolean;
|
||||
markerFile?: string;
|
||||
unitWhenGrouped?: string;
|
||||
mainRole?: string;
|
||||
length?: number;
|
||||
carrierFilename?: string;
|
||||
}
|
||||
|
||||
export interface AirbaseOptions {
|
||||
name: string;
|
||||
position: L.LatLng;
|
||||
name: string;
|
||||
position: L.LatLng;
|
||||
}
|
||||
|
||||
export interface AirbaseChartData {
|
||||
elevation: string;
|
||||
ICAO: string;
|
||||
TACAN: string;
|
||||
runways: AirbaseChartRunwayData[];
|
||||
elevation: string;
|
||||
ICAO: string;
|
||||
TACAN: string;
|
||||
runways: AirbaseChartRunwayData[];
|
||||
}
|
||||
|
||||
export interface AirbaseChartRunwayHeadingData {
|
||||
[index: string]: {
|
||||
magHeading: string;
|
||||
ILS: string;
|
||||
};
|
||||
[index: string]: {
|
||||
magHeading: string;
|
||||
ILS: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AirbaseChartRunwayData {
|
||||
headings: AirbaseChartRunwayHeadingData[];
|
||||
length: string;
|
||||
headings: AirbaseChartRunwayHeadingData[];
|
||||
length: string;
|
||||
}
|
||||
|
||||
export interface ShortcutOptions {
|
||||
label: string;
|
||||
keyUpCallback: (e: KeyboardEvent) => void;
|
||||
keyDownCallback?: (e: KeyboardEvent) => void;
|
||||
code: string;
|
||||
altKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
label: string;
|
||||
keyUpCallback: (e: KeyboardEvent) => void;
|
||||
keyDownCallback?: (e: KeyboardEvent) => void;
|
||||
code: string;
|
||||
altKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
}
|
||||
|
||||
export interface ServerStatus {
|
||||
frameRate: number;
|
||||
load: number;
|
||||
elapsedTime: number;
|
||||
missionTime: DateAndTime["time"];
|
||||
connected: boolean;
|
||||
paused: boolean;
|
||||
frameRate: number;
|
||||
load: number;
|
||||
elapsedTime: number;
|
||||
missionTime: DateAndTime["time"];
|
||||
connected: boolean;
|
||||
paused: boolean;
|
||||
}
|
||||
|
||||
export type DrawingPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type PolygonPoints = DrawingPoint[] | DrawingPoint;
|
||||
@@ -395,36 +408,36 @@ export type PolygonPoints = DrawingPoint[] | DrawingPoint;
|
||||
export type DrawingPrimitiveType = "TextBox" | "Polygon" | "Line" | "Icon";
|
||||
|
||||
export interface Drawing {
|
||||
name: string;
|
||||
visible: boolean;
|
||||
mapX: number;
|
||||
mapY: number;
|
||||
layerName: string;
|
||||
layer: string;
|
||||
primitiveType: DrawingPrimitiveType;
|
||||
colorString: string;
|
||||
fillColorString?: string;
|
||||
borderThickness?: number;
|
||||
fontSize?: number;
|
||||
font?: string;
|
||||
text?: string;
|
||||
angle?: number;
|
||||
radius?: number;
|
||||
points?: PolygonPoints;
|
||||
style?: string;
|
||||
polygonMode?: string;
|
||||
thickness?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
closed?: boolean;
|
||||
lineMode?: string;
|
||||
hiddenOnPlanner?: boolean;
|
||||
file?: string;
|
||||
scale?: number;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
mapX: number;
|
||||
mapY: number;
|
||||
layerName: string;
|
||||
layer: string;
|
||||
primitiveType: DrawingPrimitiveType;
|
||||
colorString: string;
|
||||
fillColorString?: string;
|
||||
borderThickness?: number;
|
||||
fontSize?: number;
|
||||
font?: string;
|
||||
text?: string;
|
||||
angle?: number;
|
||||
radius?: number;
|
||||
points?: PolygonPoints;
|
||||
style?: string;
|
||||
polygonMode?: string;
|
||||
thickness?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
closed?: boolean;
|
||||
lineMode?: string;
|
||||
hiddenOnPlanner?: boolean;
|
||||
file?: string;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export enum AlarmState {
|
||||
RED = 'red',
|
||||
GREEN = 'green',
|
||||
AUTO = 'auto'
|
||||
RED = "red",
|
||||
GREEN = "green",
|
||||
AUTO = "auto",
|
||||
}
|
||||
|
||||
18
frontend/react/src/map/latlng.ts
Normal file
18
frontend/react/src/map/latlng.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -443,7 +443,7 @@ export class Map extends L.Map {
|
||||
ctrlKey: false,
|
||||
});
|
||||
|
||||
for (let contextActionName in ContextActions) {
|
||||
for (const contextActionName of Object.keys(ContextActions) as Array<keyof typeof ContextActions>) {
|
||||
const contextAction = ContextActions[contextActionName] as ContextAction;
|
||||
if (contextAction.getOptions().code) {
|
||||
getApp()
|
||||
@@ -560,10 +560,15 @@ export class Map extends L.Map {
|
||||
}
|
||||
})
|
||||
.then((res: any) => {
|
||||
if ("alt-" + theatre.toLowerCase() in res) {
|
||||
let template = `${mirror}/alt-${theatre.toLowerCase()}/{z}/{x}/{y}.png`;
|
||||
// Convert the result keys to lower case to avoid case sensitivity issues
|
||||
let key = undefined;
|
||||
if ("alt-" + theatre.toLowerCase() in res) key = "alt-" + theatre.toLowerCase();
|
||||
else if ("alt-" + theatre in res) key = "alt-" + theatre;
|
||||
|
||||
if (key) {
|
||||
let template = `${mirror}/${key}/{z}/{x}/{y}.png`;
|
||||
layers.push(
|
||||
...res["alt-" + theatre.toLowerCase()].map((layerConfig: any) => {
|
||||
...res[key].map((layerConfig: any) => {
|
||||
return new L.TileLayer(template, {
|
||||
...layerConfig,
|
||||
crossOrigin: "",
|
||||
@@ -626,13 +631,13 @@ export class Map extends L.Map {
|
||||
return this.#spawnHeading;
|
||||
}
|
||||
|
||||
addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable, quickAccessName: string) {
|
||||
addStarredSpawnRequestTable(key: string, spawnRequestTable: SpawnRequestTable, quickAccessName: string) {
|
||||
this.#starredSpawnRequestTables[key] = spawnRequestTable;
|
||||
this.#starredSpawnRequestTables[key].quickAccessName = quickAccessName;
|
||||
StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables);
|
||||
}
|
||||
|
||||
removeStarredSpawnRequestTable(key) {
|
||||
removeStarredSpawnRequestTable(key: string) {
|
||||
if (key in this.#starredSpawnRequestTables) delete this.#starredSpawnRequestTables[key];
|
||||
StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables);
|
||||
}
|
||||
@@ -673,7 +678,7 @@ export class Map extends L.Map {
|
||||
}
|
||||
|
||||
setHiddenType(key: string, value: boolean) {
|
||||
this.#hiddenTypes[key] = value;
|
||||
this.#hiddenTypes[key as keyof MapHiddenTypes] = value;
|
||||
HiddenTypesChangedEvent.dispatch(this.#hiddenTypes);
|
||||
}
|
||||
|
||||
@@ -783,13 +788,13 @@ export class Map extends L.Map {
|
||||
return smokeMarker;
|
||||
}
|
||||
|
||||
setOption(key, value) {
|
||||
setOption<K extends keyof MapOptions>(key: K, value: MapOptions[K]) {
|
||||
this.#options[key] = value;
|
||||
MapOptionsChangedEvent.dispatch(this.#options, key);
|
||||
MapOptionsChangedEvent.dispatch(this.#options, key as keyof MapOptions);
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
this.#options = { ...options };
|
||||
setOptions(options: Partial<MapOptions>) {
|
||||
this.#options = { ...this.#options, ...options } as MapOptions;
|
||||
MapOptionsChangedEvent.dispatch(this.#options);
|
||||
}
|
||||
|
||||
@@ -1055,12 +1060,18 @@ 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,
|
||||
undefined,
|
||||
(hash) => {
|
||||
(hash: string) => {
|
||||
this.addTemporaryMarker(
|
||||
e.latlng,
|
||||
this.#spawnRequestTable?.unit.unitType ?? "unknown",
|
||||
@@ -1228,7 +1239,7 @@ export class Map extends L.Map {
|
||||
this.#lastMouseCoordinates = e.latlng;
|
||||
|
||||
MouseMovedEvent.dispatch(e.latlng);
|
||||
getGroundElevation(e.latlng, (elevation) => {
|
||||
getGroundElevation(e.latlng, (elevation: number) => {
|
||||
MouseMovedEvent.dispatch(e.latlng, elevation);
|
||||
});
|
||||
|
||||
@@ -1355,8 +1366,8 @@ export class Map extends L.Map {
|
||||
.filter((unit) => !unit.getHuman());
|
||||
|
||||
Object.keys(this.#destinationPreviewMarkers).forEach((ID) => {
|
||||
this.#destinationPreviewMarkers[ID].removeFrom(this);
|
||||
delete this.#destinationPreviewMarkers[ID];
|
||||
this.#destinationPreviewMarkers[parseInt(ID)].removeFrom(this);
|
||||
delete this.#destinationPreviewMarkers[parseInt(ID)];
|
||||
});
|
||||
|
||||
if (this.#keepRelativePositions) {
|
||||
@@ -1374,7 +1385,7 @@ export class Map extends L.Map {
|
||||
#moveDestinationPreviewMarkers() {
|
||||
if (this.#keepRelativePositions) {
|
||||
Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#destinationRotationCenter, this.#destinationRotation)).forEach(([ID, latlng]) => {
|
||||
this.#destinationPreviewMarkers[ID]?.setLatLng(latlng);
|
||||
this.#destinationPreviewMarkers[parseInt(ID)]?.setLatLng(latlng);
|
||||
});
|
||||
} else {
|
||||
Object.values(this.#destinationPreviewMarkers).forEach((marker) => {
|
||||
|
||||
@@ -310,11 +310,13 @@ export class OlympusApp {
|
||||
}
|
||||
|
||||
setState(state: OlympusState, subState: OlympusSubState = NO_SUBSTATE) {
|
||||
const previousState = this.#state;
|
||||
const previousSubState = this.#subState;
|
||||
this.#state = state;
|
||||
this.#subState = subState;
|
||||
|
||||
console.log(`App state set to ${state}, substate ${subState}`);
|
||||
AppStateChangedEvent.dispatch(state, subState);
|
||||
AppStateChangedEvent.dispatch(state, subState, previousState, previousSubState);
|
||||
}
|
||||
|
||||
getState() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AudioSinksChangedEvent,
|
||||
AudioSourcesChangedEvent,
|
||||
CoalitionAreasChangedEvent,
|
||||
CustomLoadoutsUpdatedEvent,
|
||||
DrawingsUpdatedEvent,
|
||||
HotgroupsChangedEvent,
|
||||
MapSourceChangedEvent,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
SessionDataSavedEvent,
|
||||
StarredSpawnsChangedEvent,
|
||||
} from "./events";
|
||||
import { SessionData } from "./interfaces";
|
||||
import { LoadoutBlueprint, SessionData } from "./interfaces";
|
||||
import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle";
|
||||
import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon";
|
||||
import { getApp } from "./olympusapp";
|
||||
@@ -124,7 +125,7 @@ export class SessionDataManager {
|
||||
HotgroupsChangedEvent.on((hotgroups) => {
|
||||
this.#sessionData.hotgroups = {};
|
||||
Object.keys(hotgroups).forEach((hotgroup) => {
|
||||
(this.#sessionData.hotgroups as { [key: string]: number[] })[hotgroup] = hotgroups[hotgroup].map((unit) => unit.ID);
|
||||
(this.#sessionData.hotgroups as { [key: string]: number[] })[hotgroup] = hotgroups[parseInt(hotgroup)].map((unit) => unit.ID);
|
||||
});
|
||||
this.#saveSessionData();
|
||||
});
|
||||
@@ -146,6 +147,16 @@ export class SessionDataManager {
|
||||
this.#sessionData.mapSource = { id: source };
|
||||
this.#saveSessionData();
|
||||
});
|
||||
|
||||
CustomLoadoutsUpdatedEvent.on((unitName, loadout) => {
|
||||
// If the loadout is of type isPersistent, update the session data
|
||||
if (loadout.persistent) {
|
||||
if (!this.#sessionData.customLoadouts) this.#sessionData.customLoadouts = {};
|
||||
if (!this.#sessionData.customLoadouts[unitName]) this.#sessionData.customLoadouts[unitName] = [];
|
||||
this.#sessionData.customLoadouts[unitName].push({...loadout});
|
||||
}
|
||||
this.#saveSessionData();
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,8 +156,8 @@ export function OlDropdown(props: {
|
||||
data-open={open}
|
||||
className={`
|
||||
absolute z-40 divide-y divide-gray-100 overflow-y-scroll
|
||||
no-scrollbar rounded-lg bg-white p-2 shadow
|
||||
dark:bg-gray-700
|
||||
no-scrollbar rounded-lg border border-2 border-gray-600 bg-gray-700
|
||||
p-2 shadow
|
||||
data-[open='false']:hidden
|
||||
`}
|
||||
>
|
||||
@@ -187,7 +187,7 @@ export function OlDropdown(props: {
|
||||
}
|
||||
|
||||
/* Conveniency Component for dropdown elements */
|
||||
export function OlDropdownItem(props: { onClick?: () => void; className?: string; borderColor?: string; children?: string | JSX.Element | JSX.Element[] }) {
|
||||
export function OlDropdownItem(props: { onClick?: () => void; className?: string; borderColor?: string; children?: string | JSX.Element | JSX.Element[], disabled?: boolean }) {
|
||||
return (
|
||||
<button
|
||||
onClick={props.onClick ?? (() => {})}
|
||||
@@ -195,8 +195,11 @@ export function OlDropdownItem(props: { onClick?: () => void; className?: string
|
||||
${props.className ?? ""}
|
||||
flex w-full cursor-pointer select-none flex-row content-center
|
||||
rounded-md px-4 py-2
|
||||
dark:hover:bg-gray-600 dark:hover:text-white
|
||||
hover:bg-gray-100
|
||||
hover:bg-gray-600 hover:text-white
|
||||
${props.disabled ? `
|
||||
cursor-default opacity-50
|
||||
hover:bg-transparent hover:text-gray-200
|
||||
` : ``}
|
||||
`}
|
||||
style={{
|
||||
border: props.borderColor ? `2px solid ${props.borderColor}` : "2px solid transparent",
|
||||
|
||||
@@ -8,6 +8,7 @@ export function OlNumberInput(props: {
|
||||
max: number;
|
||||
minLength?: number;
|
||||
className?: string;
|
||||
internalClassName?: string;
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
tooltipRelativeToParent?: boolean;
|
||||
@@ -34,7 +35,10 @@ export function OlNumberInput(props: {
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className="relative flex max-w-[8rem] items-center"
|
||||
className={`
|
||||
relative flex max-w-[8rem] items-center
|
||||
${props.internalClassName ?? ""}
|
||||
`}
|
||||
ref={buttonRef}
|
||||
onMouseEnter={() => {
|
||||
setHoverTimeout(
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -8,8 +8,9 @@ export function Modal(props: {
|
||||
open: boolean;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
className?: string;
|
||||
size?: "sm" | "md" | "lg" | "full";
|
||||
size?: "sm" | "md" | "lg" | "full" | "tall";
|
||||
disableClose?: boolean;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const [splash, setSplash] = useState(Math.ceil(Math.random() * 7));
|
||||
|
||||
@@ -54,6 +55,14 @@ export function Modal(props: {
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
props.size === "tall"
|
||||
? `
|
||||
h-[80%] w-[800px]
|
||||
max-md:h-full max-md:w-full
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${props.size === "full" ? "h-full w-full" : ""}
|
||||
`}
|
||||
>
|
||||
@@ -90,7 +99,7 @@ export function Modal(props: {
|
||||
>
|
||||
<FaXmark
|
||||
onClick={() => {
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
props.onClose ? props.onClose() : getApp().setState(OlympusState.IDLE);
|
||||
}}
|
||||
/>{" "}
|
||||
</div>
|
||||
|
||||
180
frontend/react/src/ui/modals/imageoverlaymodal.tsx
Normal file
180
frontend/react/src/ui/modals/imageoverlaymodal.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { NO_SUBSTATE, OlympusState } from "../../constants/constants";
|
||||
import { AppStateChangedEvent } from "../../events";
|
||||
import { ImageOverlay, LatLng, LatLngBounds } from "leaflet";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { OlStringInput } from "../components/olstringinput";
|
||||
|
||||
export function ImageOverlayModal(props: { open: boolean }) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE);
|
||||
const [bound1Lat, setBound1Lat] = useState("0");
|
||||
const [bound1Lon, setBound1Lon] = useState("0");
|
||||
const [bound2Lat, setBound2Lat] = useState("0");
|
||||
const [bound2Lon, setBound2Lon] = useState("0");
|
||||
const [importData, setImportData] = useState("");
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((appState, appSubState) => {
|
||||
setAppState(appState);
|
||||
setAppSubState(appSubState);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (appState !== OlympusState.IMPORT_IMAGE_OVERLAY) return;
|
||||
|
||||
setImportData("");
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
|
||||
input.onchange = async (e) => {
|
||||
// @ts-ignore TODO
|
||||
var file = e.target?.files[0];
|
||||
var reader = new FileReader();
|
||||
// Read the file content as image data URL
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = (readerEvent) => {
|
||||
// @ts-ignore TODO
|
||||
var content = readerEvent.target.result;
|
||||
if (content) {
|
||||
setImportData(content as string);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
input.click();
|
||||
}, [appState, appSubState]);
|
||||
|
||||
return (
|
||||
<Modal open={props.open} size="sm">
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<div className={`flex flex-col justify-between gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Import Image Overlay
|
||||
</span>
|
||||
|
||||
<span className="text-gray-400">Enter the corner coordinates of the image overlay to be imported.</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-gray-300">Corner 1 latitude </div>
|
||||
<div>
|
||||
<OlStringInput
|
||||
value={String(bound1Lat)}
|
||||
onChange={(ev) => {
|
||||
setBound1Lat(ev.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-gray-300">Corner 1 longitude </div>
|
||||
<div>
|
||||
<OlStringInput
|
||||
value={String(bound1Lon)}
|
||||
onChange={(ev) => {
|
||||
setBound1Lon(ev.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-gray-300">Corner 2 latitude </div>
|
||||
<div>
|
||||
<OlStringInput
|
||||
value={String(bound2Lat)}
|
||||
onChange={(ev) => {
|
||||
setBound2Lat(ev.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-gray-300">Corner 2 longitude </div>
|
||||
<div>
|
||||
<OlStringInput
|
||||
value={String(bound2Lon)}
|
||||
onChange={(ev) => {
|
||||
setBound2Lon(ev.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`
|
||||
${(showWarning ? "text-red-500" : `
|
||||
text-gray-400
|
||||
`)}
|
||||
text-sm
|
||||
`}>
|
||||
Please enter valid latitude and longitude values in decimal degrees format (e.g. 37.7749, -122.4194). Latitude must be between -90 and 90, and longitude must be between -180 and 180.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (
|
||||
isNaN(Number(bound1Lat)) || Number(bound1Lat) < -90 || Number(bound1Lat) > 90 ||
|
||||
isNaN(Number(bound1Lon)) || Number(bound1Lon) < -180 || Number(bound1Lon) > 180 ||
|
||||
isNaN(Number(bound2Lat)) || Number(bound2Lat) < -90 || Number(bound2Lat) > 90 ||
|
||||
isNaN(Number(bound2Lon)) || Number(bound2Lon) < -180 || Number(bound2Lon) > 180
|
||||
) {
|
||||
setShowWarning(true)
|
||||
return;
|
||||
}
|
||||
setShowWarning(false)
|
||||
|
||||
const bounds = new LatLngBounds([
|
||||
[Number(bound1Lat), Number(bound1Lon)],
|
||||
[Number(bound2Lat), Number(bound2Lon)]
|
||||
]
|
||||
)
|
||||
|
||||
let overlay = new ImageOverlay(importData, bounds);
|
||||
overlay.addTo(getApp().getMap());
|
||||
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center
|
||||
gap-2 rounded-sm bg-blue-700 px-5 py-2.5 text-sm
|
||||
font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.IDLE)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2
|
||||
rounded-sm border-[1px] bg-blue-700 px-5 py-2.5
|
||||
text-sm font-medium text-white
|
||||
dark:border-gray-600 dark:bg-gray-800
|
||||
dark:text-gray-400 dark:hover:bg-gray-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
160
frontend/react/src/ui/modals/loadoutwizardmodal.tsx
Normal file
160
frontend/react/src/ui/modals/loadoutwizardmodal.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FaMagic, FaStar } from "react-icons/fa";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { NO_SUBSTATE, OlympusState } from "../../constants/constants";
|
||||
import { AppStateChangedEvent, CustomLoadoutsUpdatedEvent, SetLoadoutWizardBlueprintEvent } from "../../events";
|
||||
import { WeaponsWizard } from "../panels/components/weaponswizard";
|
||||
import { LoadoutBlueprint, LoadoutItemBlueprint, UnitBlueprint } from "../../interfaces";
|
||||
import { OlToggle } from "../components/oltoggle";
|
||||
|
||||
export function LoadoutWizardModal(props: { open: boolean }) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE);
|
||||
const [previousState, setPreviousState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [previousSubState, setPreviousSubState] = useState(NO_SUBSTATE);
|
||||
const [blueprint, setBlueprint] = useState(null as UnitBlueprint | null);
|
||||
const [isPersistent, setIsPersistent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((appState, appSubState, previousState, previousSubState) => {
|
||||
setAppState(appState);
|
||||
setAppSubState(appSubState);
|
||||
setPreviousState(previousState);
|
||||
setPreviousSubState(previousSubState);
|
||||
});
|
||||
SetLoadoutWizardBlueprintEvent.on((blueprint) => {
|
||||
setBlueprint(blueprint);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear blueprint when modal is closed
|
||||
if (!props.open) {
|
||||
setBlueprint(null);
|
||||
}
|
||||
}, [props.open]);
|
||||
|
||||
const [selectedWeapons, setSelectedWeapons] = useState({} as { [key: string]: { clsid: string; name: string; weight: number } });
|
||||
const [loadoutName, setLoadoutName] = useState("New loadout");
|
||||
const [loadoutRole, setLoadoutRole] = useState("Custom");
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedWeapons({});
|
||||
}, [props.open]);
|
||||
|
||||
// If "New Loadout" already exists in the blueprint loadouts, append a number to make it unique
|
||||
useEffect(() => {
|
||||
if (!blueprint) return;
|
||||
let name = "New loadout";
|
||||
let counter = 1;
|
||||
const existingLoadoutNames = blueprint.loadouts?.map((loadout) => loadout.name) || [];
|
||||
|
||||
while (existingLoadoutNames.includes(name)) {
|
||||
name = `New loadout ${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
setLoadoutName(name);
|
||||
}, [blueprint]);
|
||||
|
||||
return (
|
||||
<Modal open={props.open} size={"tall"} onClose={() => getApp().setState(previousState, previousSubState)}>
|
||||
<div className="flex gap-4 text-xl text-white">
|
||||
<FaMagic
|
||||
className={`
|
||||
my-auto text-4xl text-gray-300
|
||||
`}
|
||||
/>
|
||||
<div className="my-auto">Loadout wizard</div>
|
||||
</div>
|
||||
<WeaponsWizard
|
||||
selectedWeapons={selectedWeapons}
|
||||
setSelectedWeapons={setSelectedWeapons}
|
||||
weaponsByPylon={blueprint?.acceptedPayloads ?? {}}
|
||||
loadoutName={loadoutName}
|
||||
setLoadoutName={setLoadoutName}
|
||||
loadoutRole={loadoutRole}
|
||||
setLoadoutRole={setLoadoutRole}
|
||||
/>
|
||||
<div className="mt-auto flex justify-between">
|
||||
<div className="flex gap-2 text-gray-200">
|
||||
<FaStar className={`my-auto text-2xl text-gray-200`}/>
|
||||
<div className={`my-auto mr-auto`}>Keep for the rest of the session</div>
|
||||
<OlToggle toggled={isPersistent} onClick={() => setIsPersistent(!isPersistent)} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// Add a new loadout to the blueprint if it doesn't exist already
|
||||
if (blueprint) {
|
||||
const items: LoadoutItemBlueprint[] = [];
|
||||
for (const pylon in selectedWeapons) {
|
||||
const weapon = selectedWeapons[pylon];
|
||||
items.push({
|
||||
name: weapon.name,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// Group the weapon items and sum their quantities if there are duplicates
|
||||
const groupedItems: LoadoutItemBlueprint[] = [];
|
||||
const itemMap: { [key: string]: LoadoutItemBlueprint } = {};
|
||||
for (const item of items) {
|
||||
if (itemMap[item.name]) {
|
||||
itemMap[item.name].quantity += item.quantity;
|
||||
} else {
|
||||
itemMap[item.name] = { ...item };
|
||||
}
|
||||
}
|
||||
for (const itemName in itemMap) {
|
||||
groupedItems.push(itemMap[itemName]);
|
||||
}
|
||||
|
||||
// Assemble the loadout payload section as a stringified lua table containing the payload number as key and the clsid as values
|
||||
// This must already be lua compatible
|
||||
let payloadLuaTable = "{pylons = {";
|
||||
for (const pylon in selectedWeapons) {
|
||||
const weapon = selectedWeapons[pylon];
|
||||
if (weapon) payloadLuaTable += `[${pylon}] = {CLSID = "${weapon.clsid}"},`;
|
||||
}
|
||||
payloadLuaTable += "}, fuel = 999999, flare=60, chaff=60, gun=100, ammo_type = 1}";
|
||||
|
||||
const newLoadout: LoadoutBlueprint = {
|
||||
items: groupedItems,
|
||||
roles: [loadoutRole],
|
||||
code: "",
|
||||
name: loadoutName,
|
||||
enabled: true,
|
||||
isCustom: true,
|
||||
persistent: isPersistent,
|
||||
payload: payloadLuaTable,
|
||||
};
|
||||
|
||||
if (!blueprint.loadouts) {
|
||||
blueprint.loadouts = [];
|
||||
}
|
||||
blueprint.loadouts.push(newLoadout);
|
||||
CustomLoadoutsUpdatedEvent.dispatch(blueprint.name, newLoadout);
|
||||
}
|
||||
getApp().setState(previousState, previousSubState);
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
35
frontend/react/src/ui/panels/components/loadoutviewer.tsx
Normal file
35
frontend/react/src/ui/panels/components/loadoutviewer.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
|
||||
export function LoadoutViewer(props: { spawnLoadout: { items: { name: string; quantity: number }[] } }) {
|
||||
return (
|
||||
<div>
|
||||
{props.spawnLoadout.items.map((item) => {
|
||||
return (
|
||||
<div
|
||||
className={`flex content-center gap-2`}
|
||||
key={item.name}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
my-auto w-6 min-w-6 rounded-full py-0.5
|
||||
text-center text-sm font-bold text-gray-500
|
||||
dark:bg-[#17212D]
|
||||
`}
|
||||
>
|
||||
{item.quantity}x
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
my-auto overflow-hidden text-ellipsis text-nowrap
|
||||
text-sm
|
||||
dark:text-gray-300
|
||||
`}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
438
frontend/react/src/ui/panels/components/weaponswizard.tsx
Normal file
438
frontend/react/src/ui/panels/components/weaponswizard.tsx
Normal file
@@ -0,0 +1,438 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FaArrowsRotate, FaTrash, FaXmark } from "react-icons/fa6";
|
||||
import { OlSearchBar } from "../../components/olsearchbar";
|
||||
import { OlToggle } from "../../components/oltoggle";
|
||||
export function WeaponsWizard(props: {
|
||||
selectedWeapons: { [key: string]: { clsid: string; name: string; weight: number } };
|
||||
setSelectedWeapons: (weapons: { [key: string]: { clsid: string; name: string; weight: number } }) => void;
|
||||
weaponsByPylon: { [key: string]: { clsid: string; name: string; weight: number }[] };
|
||||
loadoutName: string;
|
||||
setLoadoutName: (name: string) => void;
|
||||
loadoutRole: string;
|
||||
setLoadoutRole: (role: string) => void;
|
||||
}) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [selectedPylons, setSelectedPylons] = useState<string[]>([]);
|
||||
const [autofillPylons, setAutofillPylons] = useState(false);
|
||||
const [fillEmptyOnly, setFillEmptyOnly] = useState(true);
|
||||
const [weaponLetters, setWeaponLetters] = useState<{ [key: string]: string }>({}); // Letter to weapon name mapping
|
||||
const [hoveredWeapon, setHoveredWeapon] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
// If autofill is enabled, clear selected pylons
|
||||
if (autofillPylons) {
|
||||
setSelectedPylons([]);
|
||||
}
|
||||
}, [autofillPylons]);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear search text when weaponsByPylon changes
|
||||
setSearchText("");
|
||||
setSelectedPylons([]);
|
||||
}, [props.weaponsByPylon]);
|
||||
|
||||
// Find the weapons that are availabile in all the selected pylons, meaning the intersection of the weapons in each pylon
|
||||
let availableWeapons: { clsid: string; name: string; weight: number }[] = [];
|
||||
if (autofillPylons) {
|
||||
// If autofill is enabled, show all weapons
|
||||
availableWeapons = Object.values(props.weaponsByPylon).flat();
|
||||
} else {
|
||||
if (selectedPylons.length > 0) {
|
||||
// If pylons are selected, show only weapons that are in all selected pylons
|
||||
const weaponsInSelectedPylons = selectedPylons.map((pylon) => props.weaponsByPylon[pylon] || []);
|
||||
availableWeapons = weaponsInSelectedPylons.reduce((acc, weapons) => {
|
||||
return acc.filter((w) => weapons.some((w2) => w2.name === w.name));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
availableWeapons.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Remove duplicates
|
||||
availableWeapons = availableWeapons.filter((weapon, index, self) => index === self.findIndex((w) => w.name === weapon.name));
|
||||
|
||||
// Filter by search text
|
||||
if (searchText.trim() !== "") {
|
||||
availableWeapons = availableWeapons.filter((weapon) => weapon.name.toLowerCase().includes(searchText.toLowerCase()));
|
||||
}
|
||||
|
||||
// If autofill is enabled and fillEmptyOnly is enabled, remove weapons that have no compatible empty pylons
|
||||
if (autofillPylons && fillEmptyOnly) {
|
||||
availableWeapons = availableWeapons.filter((weapon) => {
|
||||
// Check if there is at least one pylon that is compatible with this weapon and is empty
|
||||
return Object.keys(props.weaponsByPylon).some((pylon) => {
|
||||
const weaponsInPylon = props.weaponsByPylon[pylon];
|
||||
return weaponsInPylon.some((w) => w.name === weapon.name) && !props.selectedWeapons[pylon];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Assign a letter to each indiviual type of weapon selected in selectedWeapons for display in the pylon selection
|
||||
// Find the first unused letter
|
||||
Object.values(props.selectedWeapons).forEach((weapon) => {
|
||||
if (Object.entries(weaponLetters).findIndex(([letter, name]) => name === weapon.name) === -1) {
|
||||
// Find the first unused letter starting from A
|
||||
let currentLetter = "A";
|
||||
while (weaponLetters[currentLetter]) {
|
||||
currentLetter = String.fromCharCode(currentLetter.charCodeAt(0) + 1);
|
||||
}
|
||||
weaponLetters[currentLetter] = weapon.name;
|
||||
currentLetter = String.fromCharCode(currentLetter.charCodeAt(0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove letters for weapons that are no longer selected
|
||||
Object.entries(weaponLetters).forEach(([letter, name]) => {
|
||||
if (Object.values(props.selectedWeapons).findIndex((weapon) => weapon.name === name) === -1) {
|
||||
delete weaponLetters[letter];
|
||||
}
|
||||
});
|
||||
|
||||
if (JSON.stringify(weaponLetters) !== JSON.stringify(weaponLetters)) setWeaponLetters({ ...weaponLetters });
|
||||
|
||||
// List of very bright and distinct colors
|
||||
const colors = {
|
||||
A: "#FF5733",
|
||||
B: "#33FF57",
|
||||
C: "#3357FF",
|
||||
D: "#F333FF",
|
||||
E: "#33FFF5",
|
||||
F: "#F5FF33",
|
||||
G: "#FF33A8",
|
||||
H: "#A833FF",
|
||||
I: "#33FFA8",
|
||||
J: "#FFA833",
|
||||
K: "#33A8FF",
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-6 text-white" onMouseEnter={() => setHoveredWeapon("")}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="my-auto font-semibold">Loadout Name</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.loadoutName}
|
||||
onChange={(e) => props.setLoadoutName(e.target.value)}
|
||||
className={`
|
||||
rounded-md border border-gray-300 bg-gray-800 p-2
|
||||
text-sm text-white
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="my-auto font-semibold">Loadout Role</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.loadoutRole}
|
||||
onChange={(e) => props.setLoadoutRole(e.target.value)}
|
||||
className={`
|
||||
rounded-md border border-gray-300 bg-gray-800 p-2
|
||||
text-sm text-white
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-400">Select weapons for each pylon</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="mx-auto flex flex-col gap-2">
|
||||
{/* Draw an airplane seen from the front using only gray lines */}
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`
|
||||
border-b-2 border- b-2 w-full border-gray-300
|
||||
`}
|
||||
></div>
|
||||
<div
|
||||
className={`
|
||||
h-14 min-w-14 rounded-full border-2
|
||||
border-gray-300
|
||||
`}
|
||||
></div>
|
||||
<div
|
||||
className={`
|
||||
border-b-2 border- b-2 w-full border-gray-300
|
||||
`}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-1">
|
||||
{Object.keys(props.weaponsByPylon).map((pylon) => {
|
||||
let weapon = props.selectedWeapons[pylon];
|
||||
let letter = Object.entries(weaponLetters).find(([letter, name]) => name === weapon?.name)?.[0] || "";
|
||||
// If the currently hovered weapon is compatible with this pylon, show "Hovered" else "Not Hovered"
|
||||
let isHovered = props.weaponsByPylon[pylon].some((w) => w.name === hoveredWeapon);
|
||||
return (
|
||||
<div key={pylon} className={``}>
|
||||
<div
|
||||
className={`
|
||||
flex h-20 flex-col items-center
|
||||
justify-center rounded-md border
|
||||
px-1
|
||||
${
|
||||
autofillPylons
|
||||
? `text-gray-400`
|
||||
: `
|
||||
cursor-pointer
|
||||
hover:bg-gray-700
|
||||
`
|
||||
}
|
||||
${
|
||||
selectedPylons.includes(pylon)
|
||||
? `
|
||||
border-gray-200
|
||||
`
|
||||
: `
|
||||
border-transparent
|
||||
`
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
if (autofillPylons) return;
|
||||
if (selectedPylons.includes(pylon)) {
|
||||
setSelectedPylons(selectedPylons.filter((p) => p !== pylon));
|
||||
} else {
|
||||
setSelectedPylons([...selectedPylons, pylon]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={`text-center text-xs`}>{pylon}</div>
|
||||
|
||||
<div
|
||||
data-autofill={autofillPylons ? "true" : "false"}
|
||||
className={`
|
||||
h-3 w-0 border
|
||||
data-[autofill='false']:border-white
|
||||
data-[autofill='true']:border-gray-400
|
||||
`}
|
||||
></div>
|
||||
{props.selectedWeapons[pylon] ? (
|
||||
<div
|
||||
data-autofill={autofillPylons ? "true" : "false"}
|
||||
data-hovered={isHovered ? "true" : "false"}
|
||||
className={`
|
||||
flex h-6 w-6 items-center
|
||||
justify-center
|
||||
rounded-full border
|
||||
data-[autofill='false']:border-white
|
||||
data-[autofill='true']:border-gray-400
|
||||
data-[hovered='true']:border-green-400
|
||||
`}
|
||||
>
|
||||
{/* Show the letter of the group the weapon belongs to from weaponLetters */}
|
||||
<span
|
||||
className={`
|
||||
text-sm font-bold
|
||||
`}
|
||||
style={{
|
||||
color: letter in colors ? colors[letter as keyof typeof colors] : "inherit",
|
||||
}}
|
||||
>
|
||||
{letter}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
data-autofill={autofillPylons ? "true" : "false"}
|
||||
data-hovered={isHovered ? "true" : "false"}
|
||||
className={`
|
||||
h-6 w-6 rounded-full
|
||||
border
|
||||
data-[autofill='false']:border-white
|
||||
data-[autofill='true']:border-gray-400
|
||||
data-[hovered='true']:border-green-400
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/* List all the groups from weaponLetters */}
|
||||
<div className="flex flex-col gap-1">
|
||||
{Object.entries(weaponLetters).map(([letter, weapon]) => (
|
||||
<div
|
||||
key={letter}
|
||||
className={`
|
||||
flex items-center text-sm
|
||||
`}
|
||||
>
|
||||
<span className="font-bold" style={{ color: letter in colors ? colors[letter as keyof typeof colors] : "inherit" }}>
|
||||
{letter}:
|
||||
</span>
|
||||
<span className="ml-1 text-gray-400">{weapon}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Buttons to select/deselect all pylons, clear all weapons and remove weapons from selected pylons */}
|
||||
<div>
|
||||
<div className="flex justify-center gap-2">
|
||||
{selectedPylons.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
className={`
|
||||
text-nowrap rounded-md bg-gray-700
|
||||
px-2 py-1 text-sm
|
||||
hover:bg-gray-600
|
||||
`}
|
||||
onClick={() => {
|
||||
setSelectedPylons([]);
|
||||
}}
|
||||
>
|
||||
<FaArrowsRotate className="inline" /> Reset selection
|
||||
</button>
|
||||
|
||||
{
|
||||
/* Checjk if any of the selected pylons have a weapon selected */
|
||||
props.selectedWeapons && selectedPylons.some((pylon) => props.selectedWeapons[pylon] !== undefined) && (
|
||||
<button
|
||||
className={`
|
||||
text-nowrap rounded-md
|
||||
bg-gray-700 px-2 py-1 text-sm
|
||||
hover:bg-gray-600
|
||||
`}
|
||||
onClick={() => {
|
||||
// Remove weapons from selected pylons
|
||||
let newSelectedWeapons = { ...props.selectedWeapons };
|
||||
selectedPylons.forEach((pylon) => {
|
||||
delete newSelectedWeapons[pylon];
|
||||
});
|
||||
props.setSelectedWeapons(newSelectedWeapons);
|
||||
}}
|
||||
>
|
||||
<FaXmark
|
||||
className={`
|
||||
inline text-red-500
|
||||
`}
|
||||
/>{" "}
|
||||
Remove
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
{props.selectedWeapons && Object.keys(props.selectedWeapons).length > 0 && (
|
||||
<button
|
||||
className={`
|
||||
text-nowrap rounded-md bg-gray-700 px-2
|
||||
py-1 text-sm
|
||||
hover:bg-gray-600
|
||||
`}
|
||||
onClick={() => {
|
||||
// Clear all selected weapons
|
||||
props.setSelectedWeapons({});
|
||||
}}
|
||||
>
|
||||
<FaTrash className="inline text-red-500" /> Delete all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="ml-2 text-sm">Autofill compatible pylons with weapon</span>
|
||||
<OlToggle
|
||||
toggled={autofillPylons}
|
||||
onClick={() => {
|
||||
setAutofillPylons(!autofillPylons);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{autofillPylons && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="ml-2 text-sm">Only fill empty pylons</span>
|
||||
<OlToggle
|
||||
toggled={fillEmptyOnly}
|
||||
onClick={() => {
|
||||
setFillEmptyOnly(!fillEmptyOnly);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<OlSearchBar onChange={setSearchText} text={searchText} />
|
||||
|
||||
<div
|
||||
className={`
|
||||
flex max-h-48 flex-col overflow-y-auto border
|
||||
border-gray-700 px-2
|
||||
`}
|
||||
>
|
||||
{selectedPylons.length === 0 && !autofillPylons && (
|
||||
<div
|
||||
className={`
|
||||
p-2 text-sm text-gray-400
|
||||
`}
|
||||
>
|
||||
No pylons selected
|
||||
</div>
|
||||
)}
|
||||
{availableWeapons.length === 0 && selectedPylons.length !== 0 && !autofillPylons && (
|
||||
<div
|
||||
className={`
|
||||
p-2 text-sm text-gray-400
|
||||
`}
|
||||
>
|
||||
No weapons compatible with all selected pylons
|
||||
</div>
|
||||
)}
|
||||
{availableWeapons.length === 0 && selectedPylons.length === 0 && autofillPylons && (
|
||||
<div
|
||||
className={`
|
||||
p-2 text-sm text-gray-400
|
||||
`}
|
||||
>
|
||||
No empty pylons available
|
||||
</div>
|
||||
)}
|
||||
|
||||
{availableWeapons.length !== 0 &&
|
||||
availableWeapons.map((weapon) => (
|
||||
<div
|
||||
key={weapon.name}
|
||||
onClick={() => {
|
||||
if (autofillPylons) {
|
||||
// Autofill all compatible pylons with the selected weapon
|
||||
let newSelectedWeapons = { ...props.selectedWeapons };
|
||||
Object.keys(props.weaponsByPylon).forEach((pylon) => {
|
||||
const weaponsInPylon = props.weaponsByPylon[pylon];
|
||||
if (fillEmptyOnly && props.selectedWeapons[pylon]) {
|
||||
// If "Only fill empty pylons" is enabled, skip filled pylons
|
||||
return;
|
||||
}
|
||||
if (weaponsInPylon.some((w) => w.name === weapon.name)) {
|
||||
newSelectedWeapons[pylon] = weapon;
|
||||
}
|
||||
});
|
||||
props.setSelectedWeapons(newSelectedWeapons);
|
||||
} else {
|
||||
let newSelectedWeapons = { ...props.selectedWeapons };
|
||||
// Add the weapon to the selected pylons
|
||||
selectedPylons.forEach((pylon) => {
|
||||
newSelectedWeapons[pylon] = weapon;
|
||||
});
|
||||
props.setSelectedWeapons(newSelectedWeapons);
|
||||
setSelectedPylons([]);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setHoveredWeapon(weapon.name)}
|
||||
onMouseLeave={() => setHoveredWeapon("")}
|
||||
className={`
|
||||
cursor-pointer rounded-md p-1 text-sm
|
||||
hover:bg-gray-700
|
||||
`}
|
||||
>
|
||||
{weapon.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -142,6 +142,31 @@ export function MainMenu(props: { open: boolean; onClose: () => void; children?:
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex cursor-pointer select-none content-center gap-3
|
||||
rounded-md p-2
|
||||
dark:hover:bg-olympus-500
|
||||
hover:bg-gray-900/10
|
||||
`}
|
||||
|
||||
onClick={() => {
|
||||
getApp().setState(OlympusState.IMPORT_IMAGE_OVERLAY);
|
||||
}}
|
||||
>
|
||||
{/*<FontAwesomeIcon icon={faFileImport} className="my-auto w-4 text-gray-800 dark:text-gray-500" />*/}
|
||||
Import image overlay
|
||||
<div className={`ml-auto flex items-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRightLong}
|
||||
className={`
|
||||
my-auto px-2 text-right text-gray-800 transition-transform
|
||||
dark:text-olympus-50
|
||||
group-hover:translate-x-2
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import { UnitControlMenu } from "./panels/unitcontrolmenu";
|
||||
import { MainMenu } from "./panels/mainmenu";
|
||||
import { SideBar } from "./panels/sidebar";
|
||||
import { OptionsMenu } from "./panels/optionsmenu";
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, UnitControlSubState } from "../constants/constants";
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, SpawnSubState, UnitControlSubState } from "../constants/constants";
|
||||
import { getApp, setupApp } from "../olympusapp";
|
||||
import { LoginModal } from "./modals/loginmodal";
|
||||
|
||||
@@ -31,6 +31,8 @@ import { ImportExportModal } from "./modals/importexportmodal";
|
||||
import { WarningModal } from "./modals/warningmodal";
|
||||
import { TrainingModal } from "./modals/trainingmodal";
|
||||
import { AdminModal } from "./modals/adminmodal";
|
||||
import { ImageOverlayModal } from "./modals/imageoverlaymodal";
|
||||
import { LoadoutWizardModal } from "./modals/loadoutwizardmodal";
|
||||
|
||||
export function UI() {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
@@ -74,6 +76,8 @@ export function UI() {
|
||||
<WarningModal open={appState === OlympusState.WARNING} />
|
||||
<TrainingModal open={appState === OlympusState.TRAINING} />
|
||||
<AdminModal open={appState === OlympusState.ADMIN} />
|
||||
<ImageOverlayModal open={appState === OlympusState.IMPORT_IMAGE_OVERLAY} />
|
||||
<LoadoutWizardModal open={appState === OlympusState.SPAWN && appSubState === SpawnSubState.LOADOUT_WIZARD} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { GAME_MASTER } from "../../constants/constants";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { UnitDatabaseLoadedEvent } from "../../events";
|
||||
import { SessionDataLoadedEvent, UnitDatabaseLoadedEvent } from "../../events";
|
||||
|
||||
export class UnitDatabase {
|
||||
blueprints: { [key: string]: UnitBlueprint } = {};
|
||||
|
||||
constructor() {}
|
||||
constructor() {
|
||||
SessionDataLoadedEvent.on((sessionData) => {
|
||||
// Check if the sessionData customloadouts contains any loadouts for units, and if so, update the blueprints
|
||||
if (sessionData.customLoadouts) {
|
||||
for (let unitName in sessionData.customLoadouts) {
|
||||
if (this.blueprints[unitName]) {
|
||||
if (!this.blueprints[unitName].loadouts) this.blueprints[unitName].loadouts = [];
|
||||
sessionData.customLoadouts[unitName].forEach((loadout) => {
|
||||
this.blueprints[unitName].loadouts?.push(loadout);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
load(url: string, category?: string) {
|
||||
if (url !== "") {
|
||||
@@ -204,7 +218,7 @@ export class UnitDatabase {
|
||||
getLoadoutNamesByRole(name: string, role: string) {
|
||||
var filteredBlueprints = this.getBlueprints();
|
||||
var loadoutsByRole: string[] = [];
|
||||
var loadouts = filteredBlueprints[name].loadouts;
|
||||
var loadouts = filteredBlueprints[name as any].loadouts;
|
||||
if (loadouts) {
|
||||
for (let loadout of loadouts) {
|
||||
if (loadout.roles.includes(role) || loadout.roles.includes("")) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -35,7 +35,7 @@ mist = {}
|
||||
-- don't change these
|
||||
mist.majorVersion = 4
|
||||
mist.minorVersion = 5
|
||||
mist.build = 122
|
||||
mist.build = 125
|
||||
|
||||
-- forward declaration of log shorthand
|
||||
local log
|
||||
@@ -695,7 +695,6 @@ do -- the main scope
|
||||
["FARP"] = "farps",
|
||||
["Fueltank"] = "fueltank_cargo",
|
||||
["Gate"] = "gate",
|
||||
["FARP Fuel Depot"] = "gsm rus",
|
||||
["Armed house"] = "home1_a",
|
||||
["FARP Command Post"] = "kp-ug",
|
||||
["Watch Tower Armed"] = "ohr-vyshka",
|
||||
@@ -704,7 +703,6 @@ do -- the main scope
|
||||
["Pipes big"] = "pipes_big_cargo",
|
||||
["Oil platform"] = "plavbaza",
|
||||
["Tetrapod"] = "tetrapod_cargo",
|
||||
["Fuel tank"] = "toplivo",
|
||||
["Trunks long"] = "trunks_long_cargo",
|
||||
["Trunks small"] = "trunks_small_cargo",
|
||||
["Passenger liner"] = "yastrebow",
|
||||
@@ -1152,6 +1150,7 @@ do -- the main scope
|
||||
end
|
||||
end
|
||||
end
|
||||
--dbLog:warn(newTable)
|
||||
--mist.debug.writeData(mist.utils.serialize,{'msg', newTable}, timer.getAbsTime() ..'Group.lua')
|
||||
newTable.timeAdded = timer.getAbsTime() -- only on the dynGroupsAdded table. For other reference, see start time
|
||||
--mist.debug.dumpDBs()
|
||||
@@ -1493,7 +1492,7 @@ do -- the main scope
|
||||
task.t = timer.getTime() + task.rep --schedule next run
|
||||
local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))
|
||||
if not err then
|
||||
log:error('Error in scheduled function: $1' .. errmsg)
|
||||
log:error('Error in scheduled function: $1', errmsg)
|
||||
end
|
||||
--scheduledTasks[i].f(unpack(scheduledTasks[i].vars, 1, table.maxn(scheduledTasks[i].vars))) -- do the task
|
||||
i = i + 1
|
||||
@@ -1519,7 +1518,7 @@ do -- the main scope
|
||||
id = tostring(original_id) .. ' #' .. tostring(id_ind)
|
||||
id_ind = id_ind + 1
|
||||
end
|
||||
|
||||
local valid
|
||||
if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then
|
||||
--log:info('object found in alive_units')
|
||||
val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_])
|
||||
@@ -1532,6 +1531,7 @@ do -- the main scope
|
||||
--trigger.action.outText('remove via death: ' .. Unit.getName(val.object),20)
|
||||
mist.DBs.activeHumans[Unit.getName(val.object)] = nil
|
||||
end]]
|
||||
valid = true
|
||||
elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then -- it didn't exist in alive_units, check old_alive_units
|
||||
--log:info('object found in old_alive_units')
|
||||
val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])
|
||||
@@ -1540,32 +1540,37 @@ do -- the main scope
|
||||
val.objectPos = pos.p
|
||||
end
|
||||
val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category
|
||||
|
||||
valid = true
|
||||
else --attempt to determine if static object...
|
||||
--log:info('object not found in alive units or old alive units')
|
||||
local pos = Object.getPosition(val.object)
|
||||
if pos then
|
||||
local static_found = false
|
||||
for ind, static in pairs(mist.DBs.unitsByCat.static) do
|
||||
if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...
|
||||
--log:info('correlated dead static object to position')
|
||||
val.objectData = static
|
||||
val.objectPos = pos.p
|
||||
val.objectType = 'static'
|
||||
static_found = true
|
||||
break
|
||||
if Object.isExist(val.object) then
|
||||
local pos = Object.getPosition(val.object)
|
||||
if pos then
|
||||
local static_found = false
|
||||
for ind, static in pairs(mist.DBs.unitsByCat.static) do
|
||||
if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...
|
||||
--log:info('correlated dead static object to position')
|
||||
val.objectData = static
|
||||
val.objectPos = pos.p
|
||||
val.objectType = 'static'
|
||||
static_found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not static_found then
|
||||
val.objectPos = pos.p
|
||||
val.objectType = 'building'
|
||||
val.typeName = Object.getTypeName(val.object)
|
||||
end
|
||||
else
|
||||
val.objectType = 'unknown'
|
||||
end
|
||||
if not static_found then
|
||||
val.objectPos = pos.p
|
||||
val.objectType = 'building'
|
||||
val.typeName = Object.getTypeName(val.object)
|
||||
end
|
||||
else
|
||||
val.objectType = 'unknown'
|
||||
valid = true
|
||||
end
|
||||
end
|
||||
mist.DBs.deadObjects[id] = val
|
||||
if valid then
|
||||
mist.DBs.deadObjects[id] = val
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2019,7 +2024,7 @@ do -- the main scope
|
||||
|
||||
end
|
||||
end
|
||||
--mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupPushedToAddGroup.lua')
|
||||
--mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, newGroup.name ..'.lua')
|
||||
--log:warn(newGroup)
|
||||
-- sanitize table
|
||||
newGroup.groupName = nil
|
||||
@@ -3560,7 +3565,7 @@ function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_ty
|
||||
end
|
||||
|
||||
function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)
|
||||
log:info("$1, $2, $3, $4, $5", unitset1, altoffset1, unitset2, altoffset2, radius)
|
||||
--log:info("$1, $2, $3, $4, $5", unitset1, altoffset1, unitset2, altoffset2, radius)
|
||||
radius = radius or math.huge
|
||||
local unit_info1 = {}
|
||||
local unit_info2 = {}
|
||||
@@ -3568,21 +3573,25 @@ function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)
|
||||
-- get the positions all in one step, saves execution time.
|
||||
for unitset1_ind = 1, #unitset1 do
|
||||
local unit1 = Unit.getByName(unitset1[unitset1_ind])
|
||||
local lCat = Object.getCategory(unit1)
|
||||
if unit1 and ((lCat == 1 and unit1:isActive()) or lCat ~= 1) and unit:isExist() == true then
|
||||
unit_info1[#unit_info1 + 1] = {}
|
||||
unit_info1[#unit_info1].unit = unit1
|
||||
unit_info1[#unit_info1].pos = unit1:getPosition().p
|
||||
if unit1 then
|
||||
local lCat = Object.getCategory(unit1)
|
||||
if ((lCat == 1 and unit1:isActive()) or lCat ~= 1) and unit1:isExist() == true then
|
||||
unit_info1[#unit_info1 + 1] = {}
|
||||
unit_info1[#unit_info1].unit = unit1
|
||||
unit_info1[#unit_info1].pos = unit1:getPosition().p
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for unitset2_ind = 1, #unitset2 do
|
||||
local unit2 = Unit.getByName(unitset2[unitset2_ind])
|
||||
local lCat = Object.getCategory(unit2)
|
||||
if unit2 and ((lCat == 1 and unit2:isActive()) or lCat ~= 1) and unit:isExist() == true then
|
||||
unit_info2[#unit_info2 + 1] = {}
|
||||
unit_info2[#unit_info2].unit = unit2
|
||||
unit_info2[#unit_info2].pos = unit2:getPosition().p
|
||||
if unit2 then
|
||||
local lCat = Object.getCategory(unit2)
|
||||
if ((lCat == 1 and unit2:isActive()) or lCat ~= 1) and unit2:isExist() == true then
|
||||
unit_info2[#unit_info2 + 1] = {}
|
||||
unit_info2[#unit_info2].unit = unit2
|
||||
unit_info2[#unit_info2].pos = unit2:getPosition().p
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4012,13 +4021,14 @@ do -- group functions scope
|
||||
|
||||
if Group.getByName(gpName) and Group.getByName(gpName):isExist() == true then
|
||||
local newGroup = Group.getByName(gpName)
|
||||
local newData = {}
|
||||
local newData = mist.utils.deepCopy(dbData)
|
||||
newData.name = gpName
|
||||
newData.groupId = tonumber(newGroup:getID())
|
||||
newData.category = newGroup:getCategory()
|
||||
newData.groupName = gpName
|
||||
newData.hidden = dbData.hidden
|
||||
|
||||
|
||||
|
||||
if newData.category == 2 then
|
||||
newData.category = 'vehicle'
|
||||
elseif newData.category == 3 then
|
||||
@@ -5193,7 +5203,8 @@ do -- mist.util scope
|
||||
|
||||
function mist.utils.getHeadingPoints(point1, point2, north) -- sick of writing this out.
|
||||
if north then
|
||||
return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1)), (mist.utils.makeVec3(point1)))
|
||||
local p1 = mist.utils.get3DDist(point1)
|
||||
return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), p1), p1)
|
||||
else
|
||||
return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1)))
|
||||
end
|
||||
@@ -5837,8 +5848,8 @@ do -- mist.debug scope
|
||||
log:alert('insufficient libraries to run mist.debug.dump_G, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua')
|
||||
--trigger.action.outText(errmsg, 10)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
--- Write debug data to file.
|
||||
-- This function requires you to disable script sanitization
|
||||
-- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io
|
||||
@@ -7653,7 +7664,10 @@ do
|
||||
--log:warn(s)
|
||||
if type(s) == 'table' then
|
||||
local mType = s.markType
|
||||
if mType == 'panel' then
|
||||
--log:echo(s)
|
||||
|
||||
if mType == 'panel' then
|
||||
local markScope = s.markScope or "all"
|
||||
if markScope == 'coa' then
|
||||
trigger.action.markToCoalition(s.markId, s.text, s.pos, s.markFor, s.readOnly)
|
||||
elseif markScope == 'group' then
|
||||
@@ -7711,10 +7725,15 @@ do
|
||||
|
||||
local function validateColor(val)
|
||||
if type(val) == 'table' then
|
||||
for i = 1, #val do
|
||||
if type(val[i]) == 'number' and val[i] > 1 then
|
||||
val[i] = val[i]/255 -- convert RGB values from 0-255 to 0-1 equivilent.
|
||||
end
|
||||
for i = 1, 4 do
|
||||
if val[i] then
|
||||
if type(val[i]) == 'number' and val[i] > 1 then
|
||||
val[i] = val[i]/255 -- convert RGB values from 0-255 to 0-1 equivilent.
|
||||
end
|
||||
else
|
||||
val[i] = 0.8
|
||||
log:warn("index $1 of color to mist.marker.add was missing, defaulted to 0.8", i)
|
||||
end
|
||||
end
|
||||
elseif type(val) == 'string' then
|
||||
val = mist.utils.hexToRGB(val)
|
||||
@@ -7755,7 +7774,7 @@ do
|
||||
--log:info('create maker DB: $1', e.idx)
|
||||
mist.DBs.markList[e.idx] = {time = e.time, pos = e.pos, groupId = e.groupId, mType = 'panel', text = e.text, markId = e.idx, coalition = e.coalition}
|
||||
if e.unit then
|
||||
mist.DBs.markList[e.idx].unit = e.intiator:getName()
|
||||
mist.DBs.markList[e.idx].unit = e.initiator:getName()
|
||||
end
|
||||
--log:info(mist.marker.list[e.idx])
|
||||
end
|
||||
@@ -7778,7 +7797,7 @@ do
|
||||
else
|
||||
for mEntry, mData in pairs(mist.DBs.markList) do
|
||||
if id == mData.name or id == mData.id then
|
||||
return mData.id
|
||||
return mData.markId
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7788,11 +7807,16 @@ do
|
||||
|
||||
|
||||
local function removeMark(id)
|
||||
--log:info("Removing Mark: $1", id
|
||||
--log:info("Removing Mark: $1", id)
|
||||
local removed = false
|
||||
if type(id) == 'table' then
|
||||
for ind, val in pairs(id) do
|
||||
local r = getMarkId(val)
|
||||
local r
|
||||
if val.markId then
|
||||
r = val.markId
|
||||
else
|
||||
r = getMarkId(val)
|
||||
end
|
||||
if r then
|
||||
trigger.action.removeMark(r)
|
||||
mist.DBs.markList[r] = nil
|
||||
@@ -7802,9 +7826,11 @@ do
|
||||
|
||||
else
|
||||
local r = getMarkId(id)
|
||||
trigger.action.removeMark(r)
|
||||
mist.DBs.markList[r] = nil
|
||||
removed = true
|
||||
if r then
|
||||
trigger.action.removeMark(r)
|
||||
mist.DBs.markList[r] = nil
|
||||
removed = true
|
||||
end
|
||||
end
|
||||
return removed
|
||||
end
|
||||
@@ -7926,6 +7952,7 @@ do
|
||||
|
||||
if markForCoa then
|
||||
if type(markForCoa) == 'string' then
|
||||
--log:warn("coa is string")
|
||||
if tonumber(markForCoa) then
|
||||
coa = coas[tonumber(markForCoa)]
|
||||
markScope = 'coa'
|
||||
@@ -7940,11 +7967,10 @@ do
|
||||
end
|
||||
elseif type(markForCoa) == 'number' and markForCoa >=-1 and markForCoa <= #coas then
|
||||
coa = markForCoa
|
||||
markScore = 'coa'
|
||||
--log:warn("coa is number")
|
||||
markScope = 'coa'
|
||||
end
|
||||
|
||||
|
||||
|
||||
markFor = coa
|
||||
elseif markFor then
|
||||
if type(markFor) == 'number' then -- groupId
|
||||
if mist.DBs.groupsById[markFor] then
|
||||
@@ -8053,7 +8079,7 @@ do
|
||||
end
|
||||
for i = 1, #markForTable do
|
||||
local newId = iterate()
|
||||
local data = {markId = newId, text = text, pos = pos[i], markFor = markForTable[i], markType = 'panel', name = name, readOnly = readOnly, time = timer.getTime()}
|
||||
local data = {markId = newId, text = text, pos = pos[i], markScope = markScope, markFor = markForTable[i], markType = 'panel', name = name, readOnly = readOnly, time = timer.getTime()}
|
||||
mist.DBs.markList[newId] = data
|
||||
table.insert(list, data)
|
||||
|
||||
@@ -8177,6 +8203,7 @@ do
|
||||
end
|
||||
|
||||
function mist.marker.remove(id)
|
||||
|
||||
return removeMark(id)
|
||||
end
|
||||
|
||||
@@ -8967,8 +8994,8 @@ do -- group tasks scope
|
||||
minR = mist.utils.get2DDist(avg, zone[i])
|
||||
end
|
||||
end
|
||||
--log:warn('Radius: $1', radius)
|
||||
--log:warn('minR: $1', minR)
|
||||
--log:warn('Radius: $1', radius)
|
||||
local lSpawnPos = {}
|
||||
for j = 1, 100 do
|
||||
newCoord = mist.getRandPointInCircle(avg, radius)
|
||||
@@ -9200,7 +9227,7 @@ do -- group tasks scope
|
||||
function mist.groupIsDead(groupName) -- copy more or less from on station
|
||||
local gp = Group.getByName(groupName)
|
||||
if gp then
|
||||
if #gp:getUnits() > 0 or gp:isExist() == true then
|
||||
if #gp:getUnits() > 0 and gp:isExist() == true then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
2
scripts/python/.vscode/launch.json
vendored
2
scripts/python/.vscode/launch.json
vendored
@@ -68,7 +68,7 @@
|
||||
"args": {
|
||||
"key": "folder",
|
||||
"description": "DCS folder location",
|
||||
"default": "E:\\Eagle Dynamics\\DCS World (Open Beta)"
|
||||
"default": "C:\\Program Files\\Eagle Dynamics\\DCS World"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
64
scripts/python/API/.vscode/launch.json
vendored
Normal file
64
scripts/python/API/.vscode/launch.json
vendored
Normal 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
563
scripts/python/API/api.py
Normal 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())
|
||||
|
||||
205
scripts/python/API/audio/audio_packet.py
Normal file
205
scripts/python/API/audio/audio_packet.py
Normal 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
|
||||
75
scripts/python/API/audio/audio_recorder.py
Normal file
75
scripts/python/API/audio/audio_recorder.py
Normal 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()
|
||||
|
||||
|
||||
150
scripts/python/API/data/data_extractor.py
Normal file
150
scripts/python/API/data/data_extractor.py
Normal 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
|
||||
74
scripts/python/API/data/data_indexes.py
Normal file
74
scripts/python/API/data/data_indexes.py
Normal 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
|
||||
98
scripts/python/API/data/data_types.py
Normal file
98
scripts/python/API/data/data_types.py
Normal 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
|
||||
1
scripts/python/API/data/roes.py
Normal file
1
scripts/python/API/data/roes.py
Normal file
@@ -0,0 +1 @@
|
||||
ROES = ["", "free", "designated", "return", "hold"]
|
||||
19
scripts/python/API/data/states.py
Normal file
19
scripts/python/API/data/states.py
Normal 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"
|
||||
]
|
||||
30
scripts/python/API/data/unit_spawn_table.py
Normal file
30
scripts/python/API/data/unit_spawn_table.py
Normal 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
|
||||
}
|
||||
39525
scripts/python/API/databases/aircraftdatabase.json
Normal file
39525
scripts/python/API/databases/aircraftdatabase.json
Normal file
File diff suppressed because it is too large
Load Diff
13423
scripts/python/API/databases/groundunitdatabase.json
Normal file
13423
scripts/python/API/databases/groundunitdatabase.json
Normal file
File diff suppressed because it is too large
Load Diff
7733
scripts/python/API/databases/helicopterdatabase.json
Normal file
7733
scripts/python/API/databases/helicopterdatabase.json
Normal file
File diff suppressed because it is too large
Load Diff
3
scripts/python/API/databases/mods.json
Normal file
3
scripts/python/API/databases/mods.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
1616
scripts/python/API/databases/navyunitdatabase.json
Normal file
1616
scripts/python/API/databases/navyunitdatabase.json
Normal file
File diff suppressed because it is too large
Load Diff
196
scripts/python/API/example_disembarked_infantry.py
Normal file
196
scripts/python/API/example_disembarked_infantry.py
Normal 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()
|
||||
|
||||
|
||||
31
scripts/python/API/example_draw_argument.py
Normal file
31
scripts/python/API/example_draw_argument.py
Normal 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()
|
||||
|
||||
24
scripts/python/API/example_precise_movement.py
Normal file
24
scripts/python/API/example_precise_movement.py
Normal 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()
|
||||
|
||||
29
scripts/python/API/example_set_cargo_weight.py
Normal file
29
scripts/python/API/example_set_cargo_weight.py
Normal 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()
|
||||
|
||||
85
scripts/python/API/example_voice_control.py
Normal file
85
scripts/python/API/example_voice_control.py
Normal 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()
|
||||
625
scripts/python/API/infantry_boarding.py
Normal file
625
scripts/python/API/infantry_boarding.py
Normal file
@@ -0,0 +1,625 @@
|
||||
import asyncio
|
||||
from asyncio import Semaphore
|
||||
import json
|
||||
from random import randrange
|
||||
from api import API, Unit, UnitSpawnTable
|
||||
from math import pi
|
||||
import logging
|
||||
|
||||
#Set some globals up
|
||||
alternate_time = 300
|
||||
before_can_re_embark_time = 300
|
||||
####Transport types#####
|
||||
transport_ground = {
|
||||
"M-113": {
|
||||
"max_capacity": 4,
|
||||
"max_embark_range": 50,
|
||||
"doors": 1,
|
||||
"door_positions": [(3.35,pi),(0,0)],
|
||||
"board_positions": [(15,pi),(0,0)],
|
||||
"door_argument_nos": None,
|
||||
"door_open_thresholds": None,
|
||||
"is_rear_loader": True,
|
||||
"boarding_distance": 5
|
||||
}
|
||||
}
|
||||
|
||||
transport_helicopters = {
|
||||
"UH-1H":{
|
||||
"max_capacity": 8,
|
||||
"max_embark_range": 100,
|
||||
"doors": 2,
|
||||
"door_positions": [(2.5,-pi/2),(0.8,0),(2.5,pi/2),(0.8,0)], #two values here offset and heading offset in radians and second distance offset and heading offset in radians
|
||||
"board_positions": [(15,-pi/2),(0,0),(15,pi/2),(0,0)],
|
||||
"door_argument_nos": [43,44], #draw argument numbers for the doors
|
||||
"door_open_thresholds": [0.8,0.8], #value above which the door is considered open
|
||||
"is_rear_loader": False,
|
||||
"boarding_distance": 5
|
||||
}
|
||||
}
|
||||
|
||||
transport_types = set(transport_helicopters.keys()).union(transport_ground.keys())
|
||||
|
||||
#Infantry transport
|
||||
embarker_inf_red = {}
|
||||
embarker_inf_blue = {"Soldier M4 GRG","soldier_wwii_us"}
|
||||
embarker_types = embarker_inf_blue.union(embarker_inf_red)
|
||||
|
||||
#Time it takes after loading or unloading to swap back to the other
|
||||
|
||||
# Setup a logger for the module
|
||||
logger = logging.getLogger("infantry_transport")
|
||||
logger.setLevel(logging.INFO)
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
class Transporter(Unit):
|
||||
def __init__(self, Unit):
|
||||
self.unit = Unit
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"is_transport": self.unit.is_transport,
|
||||
"max_capacity": self.unit.max_capacity,
|
||||
"current_capacity": self.unit.current_capacity,
|
||||
"max_embark_range": self.unit.max_embark_range,
|
||||
"boarding_distance": self.unit.boarding_distance,
|
||||
"current_cargo_weight": self.unit.current_cargo_weight,
|
||||
"unit_array": [unit.ID for unit in self.unit.unit_array],
|
||||
"en_boarding_queue": [unit.ID for unit in self.unit.en_boarding_queue],
|
||||
"doors": self.unit.doors,
|
||||
"door_positions": self.unit.door_positions,
|
||||
"board_positions": self.unit.board_positions,
|
||||
"door_argument_nos": self.unit.door_argument_nos,
|
||||
"door_open_thresholds": self.unit.door_open_thresholds,
|
||||
"is_rear_loader": self.unit.is_rear_loader,
|
||||
"will_disembark": self.unit.will_disembark
|
||||
}
|
||||
|
||||
def set_as_transport(self):
|
||||
self.unit.is_transport = True
|
||||
if self.unit.name in transport_helicopters:
|
||||
if self.unit.name == "UH-1H":
|
||||
self.unit.max_capacity = transport_helicopters["UH-1H"]["max_capacity"]
|
||||
self.unit.max_embark_range = transport_helicopters["UH-1H"]["max_embark_range"]
|
||||
self.unit.boarding_distance = transport_helicopters["UH-1H"]["boarding_distance"]
|
||||
self.unit.current_capacity = 0
|
||||
self.unit.current_cargo_weight = 0
|
||||
self.unit.unit_array = []
|
||||
self.unit.en_boarding_queue = []
|
||||
self.unit.doors = transport_helicopters["UH-1H"]["doors"]
|
||||
self.unit.door_positions = transport_helicopters["UH-1H"]["door_positions"]
|
||||
self.unit.board_positions = transport_helicopters["UH-1H"]["board_positions"]
|
||||
|
||||
self.unit.door_argument_nos = transport_helicopters["UH-1H"]["door_argument_nos"]
|
||||
self.unit.will_disembark = False
|
||||
self.unit.register_draw_argument(43) #Register draw argument 43 for UH-1H
|
||||
self.unit.register_draw_argument(44)
|
||||
self.unit.door_open_thresholds = transport_helicopters["UH-1H"]["door_open_thresholds"]
|
||||
self.unit.is_rear_loader = transport_helicopters["UH-1H"]["is_rear_loader"]
|
||||
else:
|
||||
self.unit.max_capacity = 8
|
||||
self.unit.max_embark_range = 100
|
||||
self.unit.boarding_distance = 5
|
||||
self.unit.current_capacity = 0
|
||||
self.unit.current_cargo_weight = 0
|
||||
self.unit.unit_array = []
|
||||
self.unit.en_boarding_queue = []
|
||||
self.unit.doors = 1
|
||||
self.unit.door_positions = [(5,pi),(0,0)]
|
||||
self.unit.board_positions = [(15,pi),(0,0)]
|
||||
self.unit.door_argument_nos = None
|
||||
self.unit.door_open_thresholds = None
|
||||
self.unit.will_disembark = False
|
||||
self.unit.is_rear_loader = True
|
||||
|
||||
elif self.unit.name in transport_ground:
|
||||
if self.unit.name == "M-113":
|
||||
self.unit.max_capacity = transport_ground["M-113"]["max_capacity"]
|
||||
self.unit.max_embark_range = transport_ground["M-113"]["max_embark_range"]
|
||||
self.unit.boarding_distance = transport_ground["M-113"]["boarding_distance"]
|
||||
self.unit.current_capacity = 0
|
||||
self.unit.current_cargo_weight = 0
|
||||
self.unit.unit_array = []
|
||||
self.unit.en_boarding_queue = []
|
||||
self.unit.doors = transport_ground["M-113"]["doors"]
|
||||
self.unit.door_positions = transport_ground["M-113"]["door_positions"]
|
||||
self.unit.board_positions = transport_ground["M-113"]["board_positions"]
|
||||
self.unit.door_argument_nos = transport_ground["M-113"]["door_argument_nos"]
|
||||
self.unit.door_open_thresholds = transport_ground["M-113"]["door_open_thresholds"]
|
||||
self.unit.will_disembark = False
|
||||
self.unit.is_rear_loader = transport_ground["M-113"]["is_rear_loader"]
|
||||
else:
|
||||
self.unit.max_capacity = 4
|
||||
self.unit.max_embark_range = 50
|
||||
self.unit.boarding_distance = 5
|
||||
self.unit.current_capacity = 0
|
||||
self.unit.current_cargo_weight = 0
|
||||
self.unit.unit_array = []
|
||||
self.unit.en_boarding_queue = []
|
||||
self.unit.doors = 1
|
||||
self.unit.door_positions = [(5,pi),(0,0)]
|
||||
self.unit.board_positions = [(15,pi),(0,0)]
|
||||
self.unit.door_argument_nos = None
|
||||
self.unit.door_open_thresholds = None
|
||||
self.unit.will_disembark = False
|
||||
self.unit.is_rear_loader = True
|
||||
|
||||
logger.info(f"Set unit '{self.unit.name}' as transport, with {self.unit.current_capacity} / {self.unit.max_capacity}.")
|
||||
|
||||
class DisembarkedInfantry(Unit):
|
||||
def __str__(self):
|
||||
return f"DisembarkedInfrantry(unit_id={self.unit_id}, group_id={self.group_id}, position={self.position}, heading={self.heading})"
|
||||
|
||||
def __init__(self, Unit):
|
||||
self.unit = Unit
|
||||
|
||||
def disembark_from_transport(self):
|
||||
destination = self.position.project_with_bearing_and_distance(30, self.heading)
|
||||
# Set the destination for the unit
|
||||
self.set_roe(4) #set to hold fire to avoid stopping to shoot
|
||||
self.is_loadable = False
|
||||
self.set_path([destination])
|
||||
if self.check_for_enemy_in_range():
|
||||
self.set_speed(10)
|
||||
else:
|
||||
self.set_speed(3)
|
||||
self.register_on_destination_reached_callback(
|
||||
self.on_destination_reached,
|
||||
destination,
|
||||
threshold=15.0,
|
||||
timeout=30.0 # Timeout after 30 seconds if the destination is not reached
|
||||
)
|
||||
|
||||
def check_for_enemy_in_range(self):
|
||||
units = api.get_units()
|
||||
for unit in units.values():
|
||||
if unit.alive and unit.coalition != self.coalition:
|
||||
distance_to_enemy = self.position.distance_to(unit.position)
|
||||
if distance_to_enemy < 2000: #if an enemy is within 100m
|
||||
return True
|
||||
return False
|
||||
|
||||
async def on_destination_reached(self, _, reached: bool):
|
||||
if not reached:
|
||||
# logger.info(f"Unit {self} did not reach its destination.")
|
||||
self.set_roe(1)
|
||||
new_patrol = self.position.project_with_bearing_and_distance(1000, self.transport_spawn_heading)
|
||||
await asyncio.sleep(self.time_delay) #wait a bit before trying again
|
||||
self.set_path([new_patrol])
|
||||
if self.check_for_enemy_in_range():
|
||||
self.set_speed(10)
|
||||
else:
|
||||
self.set_speed(1.3)
|
||||
await asyncio.sleep(before_can_re_embark_time) #wait before setting to be boardable
|
||||
self.is_loadable = True
|
||||
logger.info(f"Unit {self} is now boardable again.")
|
||||
else:
|
||||
self.set_roe(1)
|
||||
logger.info(f"Unit {self} has reached its destination.")
|
||||
new_patrol = self.position.project_with_bearing_and_distance(1000, self.transport_spawn_heading)
|
||||
await asyncio.sleep(self.time_delay) #wait a bit before trying again
|
||||
self.set_path([new_patrol])
|
||||
if self.check_for_enemy_in_range():
|
||||
self.set_speed(10)
|
||||
else:
|
||||
self.set_speed(1.3)
|
||||
await asyncio.sleep(before_can_re_embark_time) #wait before setting to be boardable
|
||||
self.is_loadable = True
|
||||
logger.info(f"Unit {self} is now boardable again.")
|
||||
|
||||
|
||||
class Embarker(Unit):
|
||||
def __str__(self):
|
||||
return f"DisembarkedInfrantry(unit_id={self.unit_id}, group_id={self.group_id}, position={self.position}, heading={self.heading})"
|
||||
|
||||
def __init__(self, Unit):
|
||||
self.unit = Unit
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"is_embarker": self.unit.is_embarker,
|
||||
"is_moving": self.unit.is_moving,
|
||||
"is_loadable": self.unit.is_loadable,
|
||||
"in_embark_queue": self.unit.in_embark_queue if hasattr(self.unit, 'in_embark_queue') else False,
|
||||
"transport_unit": self.unit.transport_unit.ID if hasattr(self.unit, 'transport_unit') and self.unit.transport_unit else None
|
||||
}
|
||||
|
||||
def set_as_embarker(self):
|
||||
self.unit.is_embarker = True
|
||||
self.unit.is_moving = False
|
||||
self.unit.is_loadable = True
|
||||
logger.info(f"Set unit '{self.unit.name}' as embarker.")
|
||||
self.unit.set_custom_string("I am an embarker.")
|
||||
|
||||
def can_board(self):
|
||||
transport = self.transport_unit
|
||||
if transport.current_capacity < transport.max_capacity:
|
||||
transport.unit_array.append(self.name)
|
||||
transport.current_capacity += 1
|
||||
self.delete_unit()
|
||||
else:
|
||||
pass
|
||||
|
||||
def board_transport(self):
|
||||
door, num_doors_open = self.get_closest_door()
|
||||
if num_doors_open > 1: door_bypass = True
|
||||
else: door_bypass = False
|
||||
|
||||
if door is None:
|
||||
pass
|
||||
elif door is not None:
|
||||
if self.is_moving:
|
||||
pass
|
||||
elif not self.is_moving:
|
||||
distance_to_door = self.position.distance_to(door)
|
||||
distance_to_centre = self.position.distance_to(self.transport_unit.position)
|
||||
if distance_to_door < distance_to_centre:
|
||||
bearing = self.position.bearing_to(door)
|
||||
if hasattr(self,'nudge'):
|
||||
nudge_factor = self.nudge
|
||||
else:
|
||||
nudge_factor = 0
|
||||
destination = self.position.project_with_bearing_and_distance(distance_to_door+nudge_factor, bearing)
|
||||
destination.threshold = 2
|
||||
# Set the destination for the unit
|
||||
self.set_path([destination])
|
||||
self.register_on_destination_reached_callback(
|
||||
self.on_destination_reached,
|
||||
destination,
|
||||
threshold=2.0,
|
||||
timeout=10.0 # Timeout after 30 seconds if the destination is not reached
|
||||
)
|
||||
self.is_moving = True
|
||||
else:# distance_to_door >= distance_to_centre:
|
||||
if self.transport_unit.is_rear_loader:
|
||||
in_front_of_transport = self.transport_unit.position.project_with_bearing_and_distance(15, self.transport_unit.heading-pi)
|
||||
else:
|
||||
in_front_of_transport = self.transport_unit.position.project_with_bearing_and_distance(15, self.transport_unit.heading)
|
||||
bearing = self.position.bearing_to(in_front_of_transport)
|
||||
destination = self.position.project_with_bearing_and_distance(distance_to_door, bearing)
|
||||
destination.threshold = 2
|
||||
self.set_path([destination])
|
||||
self.register_on_destination_reached_callback(
|
||||
self.on_destination_reached,
|
||||
destination,
|
||||
threshold=2.0,
|
||||
timeout=10.0
|
||||
)
|
||||
self.is_moving = True
|
||||
|
||||
def get_closest_door(self):
|
||||
return check_closest_open_door(self.transport_unit, self)
|
||||
|
||||
async def on_destination_reached(self, _, reached: bool):
|
||||
if not reached:
|
||||
logger.info(f"Unit {self} did not reach its destination.")
|
||||
self.is_moving = False
|
||||
else:
|
||||
logger.info(f"Unit {self} has reached its destination.")
|
||||
self.is_moving = False
|
||||
|
||||
await asyncio.sleep(10)
|
||||
self.board_transport() # Attempt to board again
|
||||
|
||||
def check_closest_open_door(transport, embarker):
|
||||
if transport.name in transport_helicopters:
|
||||
if transport.door_argument_nos is None and transport.doors > 0:
|
||||
return transport.position.project_with_bearing_and_distance(5,transport.heading + pi), transport.heading + pi
|
||||
elif transport.door_argument_nos is not None and transport.doors > 0:
|
||||
closest_door = None
|
||||
doors_open = 0
|
||||
distance_to_closest_door = float('inf')
|
||||
for i in range(transport.doors):
|
||||
if transport.draw_arguments[i].value >= transport.door_open_thresholds[i]:
|
||||
doors_open += 1
|
||||
distance = embarker.position.distance_to(transport.position.project_with_bearing_and_distance(transport.door_positions[i*2][0], transport.heading + transport.door_positions[i*2][1]).project_with_bearing_and_distance(transport.door_positions[i*2+1][0], transport.heading + transport.door_positions[i*2+1][1]))
|
||||
if distance < distance_to_closest_door:
|
||||
distance_to_closest_door = distance
|
||||
closest_door = transport.position.project_with_bearing_and_distance(transport.door_positions[i*2][0], transport.heading + transport.door_positions[i*2][1]).project_with_bearing_and_distance(transport.door_positions[i*2+1][0], transport.heading + transport.door_positions[i*2+1][1])
|
||||
return closest_door, doors_open
|
||||
else:
|
||||
return None, 0
|
||||
elif transport.name in transport_ground:
|
||||
if transport.door_argument_nos is None and transport.doors > 0:
|
||||
return transport.position.project_with_bearing_and_distance(2,transport.heading + pi), transport.heading + pi
|
||||
elif transport.door_argument_nos is not None and transport.doors > 0:
|
||||
closest_door = None
|
||||
doors_open = 0
|
||||
distance_to_closest_door = float('inf')
|
||||
for i in range(transport.doors):
|
||||
if transport.draw_arguments[i].value >= transport.door_open_thresholds[i]:
|
||||
doors_open += 1
|
||||
distance = embarker.position.distance_to(transport.position.project_with_bearing_and_distance(transport.door_positions[i*2][0], transport.heading + transport.door_positions[i*2][1]).project_with_bearing_and_distance(transport.door_positions[i*2+1][0], transport.heading + transport.door_positions[i*2+1][1]))
|
||||
if distance < distance_to_closest_door:
|
||||
distance_to_closest_door = distance
|
||||
closest_door = transport.position.project_with_bearing_and_distance(transport.door_positions[i*2][0], transport.heading + transport.door_positions[i*2][1]).project_with_bearing_and_distance(transport.door_positions[i*2+1][0], transport.heading + transport.door_positions[i*2+1][1])
|
||||
return closest_door, doors_open
|
||||
else:
|
||||
return None, 0
|
||||
|
||||
def check_for_door_status(transporter):
|
||||
if transporter.name in transport_helicopters:
|
||||
if transporter.door_argument_nos is None and transporter.doors > 0:
|
||||
return True
|
||||
elif transporter.door_argument_nos is not None and transporter.doors > 0:
|
||||
a_door_is_open = False
|
||||
for i in range(transporter.doors):
|
||||
if transporter.draw_arguments[i].value >= transporter.door_open_thresholds[i]:
|
||||
a_door_is_open = True
|
||||
return a_door_is_open
|
||||
else:
|
||||
return False
|
||||
elif transporter.name in transport_ground:
|
||||
if transporter.door_argument_nos is None and transporter.doors > 0:
|
||||
return True
|
||||
elif transporter.door_argument_nos is not None and transporter.doors > 0:
|
||||
a_door_is_open = False
|
||||
for i in range(transporter.doors):
|
||||
if transporter.draw_arguments[i].value >= transporter.door_open_thresholds[i]:
|
||||
a_door_is_open = True
|
||||
return a_door_is_open
|
||||
else:
|
||||
return False
|
||||
|
||||
async def load_loadable_units():
|
||||
units = api.get_units()
|
||||
for embarker in units.values():
|
||||
if embarker.alive and hasattr(embarker, 'is_embarker'):
|
||||
if hasattr(embarker, 'in_embark_queue') and hasattr(embarker, 'transport_unit') and hasattr(embarker, 'is_moving'):
|
||||
if embarker.transport_unit.name in transport_types:
|
||||
#check the speed and distance, slow down if close
|
||||
distance_to_transport = embarker.position.distance_to(embarker.transport_unit.position)
|
||||
if distance_to_transport > 10 and embarker.speed < 1.4:
|
||||
embarker.set_speed(10)
|
||||
elif distance_to_transport < 10 and embarker.speed >= 3:
|
||||
embarker.set_speed(2)
|
||||
elif distance_to_transport < 5 and embarker.speed >= 1.3:
|
||||
embarker.set_speed(1.3)
|
||||
if embarker.roe != "hold":
|
||||
embarker.set_roe(4) #set to hold fire to avoid stopping to shoot
|
||||
#check the doors are open
|
||||
if check_for_door_status(embarker.transport_unit):
|
||||
closest_door, num_doors_open = check_closest_open_door(embarker.transport_unit, embarker)
|
||||
if closest_door is not None:
|
||||
#print(f"A door is open on {embarker.transport_unit.name}, closest door is {closest_door}, {num_doors_open} doors open")
|
||||
embarker.__class__ = Embarker
|
||||
#check if close enough to board
|
||||
closest_door, _ = embarker.get_closest_door()
|
||||
door_distance = embarker.position.distance_to(closest_door)
|
||||
if door_distance < embarker.transport_unit.boarding_distance:
|
||||
transport = embarker.transport_unit
|
||||
embarker_units = [
|
||||
(embarker, embarker.position.distance_to(transport.position))
|
||||
for embarker in units.values()
|
||||
if embarker.alive
|
||||
and hasattr(embarker, 'is_embarker')
|
||||
and embarker.position.distance_to(transport.position) < transport.boarding_distance
|
||||
]
|
||||
|
||||
embarkers_sorted = sorted(embarker_units, key=lambda x: x[1])
|
||||
if not embarkers_sorted:
|
||||
pass
|
||||
else:
|
||||
if embarker.ID == embarkers_sorted[0][0].ID:
|
||||
transport.current_capacity += 1
|
||||
transport.unit_array.append(embarker)
|
||||
transport.set_cargo_weight(transport.current_cargo_weight + 100) #assume 100kg per infantry with kit
|
||||
transport.current_cargo_weight += 100
|
||||
embarker.delete_unit()
|
||||
asyncio.create_task(set_as_disembarking(transport))
|
||||
break
|
||||
#else run it closer
|
||||
if embarker.is_moving:
|
||||
if hasattr(embarker, 'last_pos'):
|
||||
if embarker.position == embarker.last_pos:
|
||||
embarker.is_moving = False
|
||||
embarker.set_speed(1.3)
|
||||
if hasattr(embarker, 'nudge'):
|
||||
embarker.nudge = embarker.nudge + 2
|
||||
else:
|
||||
embarker.nudge = 2
|
||||
embarker.last_pos = embarker.position
|
||||
pass
|
||||
elif not embarker.is_moving:
|
||||
embarker.board_transport()
|
||||
else:
|
||||
#no doors so do nothing
|
||||
pass
|
||||
|
||||
def generate_transport_units():
|
||||
units = api.get_units()
|
||||
for unit in units.values():
|
||||
if unit.alive and unit.name in transport_types and not hasattr(unit, 'is_transport'):
|
||||
new_transport = Transporter(unit)
|
||||
new_transport.set_as_transport()
|
||||
|
||||
elif unit.alive and unit.name in embarker_types and not hasattr(unit, 'is_embarker'):
|
||||
new_emabarquee = Embarker(unit)
|
||||
new_emabarquee.set_as_embarker()
|
||||
|
||||
async def set_as_disembarking(transport):
|
||||
await asyncio.sleep(alternate_time)
|
||||
transport.will_disembark = True
|
||||
|
||||
async def set_as_not_disembarking(transport):
|
||||
await asyncio.sleep(alternate_time)
|
||||
transport.will_disembark = False
|
||||
|
||||
unload_semaphore = Semaphore(1)
|
||||
|
||||
async def check_for_unloadable_units():
|
||||
# Use the semaphore to ensure only one instance runs at a time
|
||||
async with unload_semaphore:
|
||||
units = api.get_units()
|
||||
try:
|
||||
for transporter in units.values():
|
||||
if transporter.alive and hasattr(transporter, 'is_transport') and transporter.will_disembark:
|
||||
# Check if the transporter is in a position to disembark units
|
||||
if transporter.speed < 2 and check_for_door_status(transporter) and not transporter.airborne: # check speed is less than 2 m/s and doors are open
|
||||
first_two_spawns = True # Track if we are handling the first two spawns
|
||||
to_remove = [] #sets up variable to hold units to remove from queue
|
||||
for disembarker in transporter.unit_array:
|
||||
# Get the open doors
|
||||
open_doors = []
|
||||
open_doors_headings = []
|
||||
for i in range(transporter.doors):
|
||||
if transporter.draw_arguments[i].value >= transporter.door_open_thresholds[i]:
|
||||
door_position = transporter.position.project_with_bearing_and_distance(
|
||||
transporter.door_positions[i * 2][0],
|
||||
transporter.heading + transporter.door_positions[i * 2][1]
|
||||
).project_with_bearing_and_distance(
|
||||
transporter.door_positions[i * 2 + 1][0],
|
||||
transporter.heading + transporter.door_positions[i * 2 + 1][1]
|
||||
)
|
||||
door_heading = transporter.heading + transporter.door_positions[i * 2][1]
|
||||
open_doors.append(door_position)
|
||||
open_doors_headings.append(door_heading)
|
||||
|
||||
# Round-robin spawn mechanism
|
||||
if not hasattr(transporter, 'last_door_index'):
|
||||
transporter.last_door_index = 0 # Initialize the last used door index
|
||||
|
||||
# Get the next door in the round-robin sequence
|
||||
door_index = transporter.last_door_index % len(open_doors)
|
||||
transporter.last_door_index += 1 # Increment the door index for the next spawn
|
||||
|
||||
# Spawn the unit at the selected door
|
||||
door_position = open_doors[door_index]
|
||||
door_heading = open_doors_headings[door_index]
|
||||
|
||||
spawn_table: UnitSpawnTable = UnitSpawnTable(
|
||||
unit_type=disembarker.name,
|
||||
location=door_position,
|
||||
heading=door_heading,
|
||||
skill="High",
|
||||
livery_id=""
|
||||
)
|
||||
|
||||
async def execution_callback(new_group_ID: int):
|
||||
logger.info(f"New units spawned, groupID: {new_group_ID}")
|
||||
units = api.get_units()
|
||||
for new_unit in units.values():
|
||||
if new_unit.group_id == new_group_ID:
|
||||
logger.info(f"New unit spawned: {new_unit}")
|
||||
new_unit.__class__ = DisembarkedInfantry
|
||||
new_unit.transport_spawn_heading = transporter.heading
|
||||
new_unit.disembark_from_transport()
|
||||
new_unit.original_position = new_unit.position
|
||||
#the delay is a function of how many units are left to disembark and how long it takes to get to the disembark spot
|
||||
new_unit.time_delay = transporter.max_capacity*2 - transporter.current_capacity # Random delay between 10 and 30 seconds
|
||||
|
||||
api.spawn_ground_units([spawn_table], transporter.coalition, "", True, 0, execution_callback)
|
||||
to_remove.append(disembarker)
|
||||
transporter.en_boarding_queue = []
|
||||
transporter.current_capacity -= 1
|
||||
transporter.set_cargo_weight(transporter.current_cargo_weight - 100) # Assume 100kg per infantry with kit
|
||||
transporter.current_cargo_weight -= 100
|
||||
|
||||
# Add a delay between spawns
|
||||
if len(open_doors) > 1 and first_two_spawns:
|
||||
# Shorter delay for the first two spawns if both doors are open
|
||||
await asyncio.sleep(0.5)
|
||||
first_two_spawns = False
|
||||
else:
|
||||
# Normal delay for subsequent spawns or single-door spawns
|
||||
await asyncio.sleep(2.5)
|
||||
for disembarker in to_remove:
|
||||
transporter.unit_array.remove(disembarker)
|
||||
if transporter.current_capacity == 0:
|
||||
await set_as_not_disembarking(transporter)
|
||||
|
||||
logger.info(f"Spawned unit '{disembarker.name}' from open door of transport '{transporter.name}'.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in check_for_unloadable_units: {e}")
|
||||
|
||||
def check_for_loadable_units():
|
||||
units = api.get_units()
|
||||
for transporter in units.values():
|
||||
if transporter.alive and hasattr(transporter, 'is_transport') and not transporter.will_disembark:
|
||||
if len(transporter.unit_array) < transporter.max_capacity:
|
||||
if transporter.speed < 2 and check_for_door_status(transporter): #check speed is less than 2 m/s and doors are open
|
||||
# print("Speed is okay")
|
||||
embarker_units = [
|
||||
(embarker, embarker.position.distance_to(transporter.position))
|
||||
for embarker in units.values()
|
||||
if embarker.alive
|
||||
and hasattr(embarker, 'is_embarker')
|
||||
and getattr(embarker, 'is_loadable', True) # Check if is_loadable is True
|
||||
and embarker.position.distance_to(transporter.position) < transporter.max_embark_range
|
||||
]
|
||||
if embarker_units is None or len(embarker_units) == 0:
|
||||
continue
|
||||
else:
|
||||
for embarker in embarker_units:
|
||||
if hasattr(embarker, 'in_embark_queue') and embarker.in_embark_queue:
|
||||
if embarker.in_embark_queue:
|
||||
embarker_units.remove(embarker)
|
||||
|
||||
embarkers_sorted = sorted(embarker_units, key=lambda x: x[1])
|
||||
closest_embarkers = embarkers_sorted[:transporter.max_capacity-len(transporter.en_boarding_queue)]
|
||||
|
||||
for embarker, distance in closest_embarkers:
|
||||
if embarker not in transporter.en_boarding_queue and distance < transporter.max_embark_range:
|
||||
transporter.en_boarding_queue.append(embarker)
|
||||
embarker.in_embark_queue = True
|
||||
embarker.transport_unit = transporter
|
||||
logger.info(f"Added embarker '{embarker.name}' to '{transporter.name}' s boarding queue.")
|
||||
elif embarker in transporter.en_boarding_queue:
|
||||
pass
|
||||
else:
|
||||
pass #we pass as the transport is full
|
||||
|
||||
|
||||
#############
|
||||
#API SECTION#
|
||||
#############
|
||||
def on_api_startup(api: API):
|
||||
global units_to_delete
|
||||
logger.info("API started")
|
||||
|
||||
# Get all the units from the API. Force an update to get the latest units.
|
||||
units = api.update_units()
|
||||
|
||||
# Initialize the list to hold units to delete
|
||||
units_to_delete = []
|
||||
|
||||
def on_unit_alive_change(unit: Unit, value: bool):
|
||||
global units_to_delete
|
||||
|
||||
if units_to_delete is None:
|
||||
logger.error("units_to_delete is not initialized.")
|
||||
return
|
||||
|
||||
# Check if the unit has been deleted
|
||||
if value is False:
|
||||
if unit in units_to_delete:
|
||||
units_to_delete.remove(unit)
|
||||
else:
|
||||
pass
|
||||
|
||||
async def update_data():
|
||||
units = api.get_units()
|
||||
for unit in units.values():
|
||||
if unit.alive and hasattr(unit, 'is_transport'):
|
||||
stringified_json = json.dumps(Transporter(unit).to_json())
|
||||
unit.set_custom_string(stringified_json)
|
||||
elif unit.alive and hasattr(unit, 'is_embarker'):
|
||||
stringified_json = json.dumps(Embarker(unit).to_json())
|
||||
unit.set_custom_string(stringified_json)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def on_api_update(api: API):
|
||||
generate_transport_units()
|
||||
check_for_loadable_units()
|
||||
asyncio.create_task(load_loadable_units())
|
||||
asyncio.create_task(check_for_unloadable_units())
|
||||
asyncio.create_task(update_data())
|
||||
|
||||
if __name__ == "__main__":
|
||||
api = API()
|
||||
api.register_on_update_callback(on_api_update)
|
||||
api.register_on_startup_callback(on_api_startup)
|
||||
api.run()
|
||||
47
scripts/python/API/olympus.json
Normal file
47
scripts/python/API/olympus.json
Normal 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
|
||||
}
|
||||
}
|
||||
414
scripts/python/API/radio/radio_listener.py
Normal file
414
scripts/python/API/radio/radio_listener.py
Normal 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()
|
||||
|
||||
803
scripts/python/API/unit/unit.py
Normal file
803
scripts/python/API/unit/unit.py
Normal 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}})
|
||||
83
scripts/python/API/utils/utils.py
Normal file
83
scripts/python/API/utils/utils.py
Normal 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
|
||||
|
||||
@@ -4,8 +4,6 @@ import inspect
|
||||
import difflib
|
||||
from slpp import slpp as lua
|
||||
|
||||
SEARCH_FOLDER = sys.argv[2]
|
||||
|
||||
from dcs.weapons_data import Weapons
|
||||
from dcs.planes import *
|
||||
from dcs.helicopters import *
|
||||
@@ -114,14 +112,14 @@ if len(sys.argv) > 1:
|
||||
database = json.load(f)
|
||||
|
||||
# Loads the loadout names
|
||||
with open('../unitPayloads.lua') as f:
|
||||
with open('unitPayloads.lua') as f:
|
||||
lines = f.readlines()
|
||||
unit_payloads = lua.decode("".join(lines).replace("Olympus.unitPayloads = ", "").replace("\n", ""))
|
||||
|
||||
# Loads the loadout roles
|
||||
with open('payloadRoles.json') as f:
|
||||
payloads_roles = json.load(f)
|
||||
|
||||
|
||||
# Loop on all the units in the database
|
||||
for unit_name in database:
|
||||
try:
|
||||
@@ -130,10 +128,6 @@ if len(sys.argv) > 1:
|
||||
unitmap = plane_map
|
||||
elif (sys.argv[1] == "helicopter"):
|
||||
unitmap = helicopter_map
|
||||
elif (sys.argv[1] == "groundunit"):
|
||||
unitmap = vehicle_map
|
||||
elif (sys.argv[1] == "navyunit"):
|
||||
unitmap = ship_map
|
||||
lowercase_keys = [key.lower() for key in unitmap.keys()]
|
||||
res = difflib.get_close_matches(unit_name.lower(), lowercase_keys)
|
||||
if len(res) > 0:
|
||||
@@ -153,6 +147,26 @@ if len(sys.argv) > 1:
|
||||
"roles": ["No task", rename_task(cls.task_default.name)]
|
||||
}
|
||||
database[unit_name]["loadouts"].append(empty_loadout)
|
||||
|
||||
pylon_usage = {}
|
||||
for pylon_name in cls.pylons:
|
||||
pylon_usage[pylon_name] = []
|
||||
# The pylon data is expressed as a class named PylonX, where X is the pylon_name
|
||||
pylon_cls_name = f'Pylon{pylon_name}'
|
||||
if hasattr(cls, pylon_cls_name):
|
||||
pylon_cls = getattr(cls, pylon_cls_name)
|
||||
# The pylon class has as many attributes as there are possible weapons for that pylon
|
||||
for attr_name in dir(pylon_cls):
|
||||
if not attr_name.startswith('__') and not callable(getattr(pylon_cls, attr_name)):
|
||||
weapon_data = getattr(pylon_cls, attr_name)
|
||||
if isinstance(weapon_data[1], dict) and "clsid" in weapon_data[1]:
|
||||
pylon_usage[pylon_name].append(weapon_data[1])
|
||||
|
||||
# Add the available pylon usage
|
||||
database[unit_name]["acceptedPayloads"] = {}
|
||||
for pylon_name in pylon_usage:
|
||||
pylon_data = pylon_usage[pylon_name]
|
||||
database[unit_name]["acceptedPayloads"][pylon_name] = pylon_usage[pylon_name]
|
||||
|
||||
# Loop on all the loadouts for that unit
|
||||
for payload_name in unit_payloads[unit_name]:
|
||||
|
||||
@@ -47,8 +47,10 @@ if len(sys.argv) > 1:
|
||||
print(f"Warning, could not find {unit_name} in classes list. Skipping...")
|
||||
continue
|
||||
|
||||
database[unit_name]["acquisitionRange"] = unitmap[found_name].detection_range
|
||||
database[unit_name]["engagementRange"] = unitmap[found_name].threat_range
|
||||
if not "acquisitionRange" in database[unit_name]:
|
||||
database[unit_name]["acquisitionRange"] = unitmap[found_name].detection_range
|
||||
if not "engagementRange" in database[unit_name]:
|
||||
database[unit_name]["engagementRange"] = unitmap[found_name].threat_range
|
||||
|
||||
except Exception as e:
|
||||
print(f"Could not find data for unitof type {unit_name}: {e}, skipping...")
|
||||
|
||||
144
scripts/python/add_loadout_types.py
Normal file
144
scripts/python/add_loadout_types.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Add a "type" entry to each loadout item in an aircraft/helicopter database JSON.
|
||||
|
||||
Usage: run from repository root (or adjust paths) using python.
|
||||
Creates a backup of the file before overwriting.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import re
|
||||
|
||||
# Paths to database files to process (aircraft + helicopter)
|
||||
DB_PATHS = [
|
||||
Path(r"c:\Users\dpass\Documents\GitHub\DCSOlympus\mock-dcs\Mods\Services\Olympus\databases\units\aircraftdatabase.json"),
|
||||
Path(r"c:\Users\dpass\Documents\GitHub\DCSOlympus\mock-dcs\Mods\Services\Olympus\databases\units\helicopterdatabase.json"),
|
||||
]
|
||||
BACKUP_SUFFIX = ".bak"
|
||||
MAP_PATH = Path(r"c:\Users\dpass\Documents\GitHub\DCSOlympus\scripts\python\loadout_type_map.json")
|
||||
|
||||
# Simple keyword -> type mapping. Order matters: first match wins.
|
||||
TYPE_MAP = [
|
||||
# Expanded A/A missiles (common and many variants)
|
||||
(r"\bAIM-?\d+\b|Sidewinder|AIM-9|R-73|R-27|R-60|R-77|R-24R|R-24T|R-33|R-40RD|R-40TD|R530|R530F|SD-10A|SD-10|MICA|Matra|Magic|Super\s*530|PL-5|PL-8|PL-5E|PL-8B|R550|S530D|S530F|RVV|AA-", "A/A missile"),
|
||||
# A/G / anti-ship missiles
|
||||
(r"\bAGM-?\d+\b|Maverick|ASM|Anti-ship|Harpoon|Kh-35|Kh-31|Exocet|AGM-65|AGM-84|Kh-22|Kh-25MPU|Kh-58U|Kh-66|YJ-12|YJ-83|YJ-83K|C-802|CM802|KD-20|KD-63|KG-600", "A/G missile"),
|
||||
# Guided bombs / glide weapons
|
||||
(r"\bGBU-?\d+\b|JDAM|Laser Guided Bomb|GBU|JSOW|LS-6|MPRL|BROACH", "Guided bomb"),
|
||||
# General purpose bombs and mk-series
|
||||
(r"\bMk-?\d+\b|\bFAB-?\d+\b|500lb|2000lb|GP Bomb|GP Bombs|Bomb|MC Mk|S.A.P\.|GP Mk", "General purpose bomb"),
|
||||
# Cluster bombs
|
||||
(r"\bCBU-?\d+\b|Cluster Bomb|BLU-|SFW|CEM", "Cluster bomb"),
|
||||
# Practice / training munitions
|
||||
(r"\bBDU-?\w*\b|Practice Bomb|Captive Trg|CAP-?\d+|CATM-?\b", "Practice/Training munition"),
|
||||
# Unguided rockets and rocket pods
|
||||
(r"\bLAU-?\d+\b|Hydra|Hydra 70|70 mm|M156|M151|MK151|APKWS|S-5M|S-8|S-13|S-25|UB-16|UB-32|RP-3|B-13L|ORO-57K|R-?|RP-3|R-?P-?3", "Unguided rocket"),
|
||||
# Targeting pods and cameras
|
||||
(r"\bAN/AAQ-?\d+\b|AN/ASQ-?\d+|Laser Spot Tracker|LST/SCAM|Targeting Pod|LITENING|TGP|TGM-?\d+|LANTIRN|FLIR|Pod", "Targeting pod"),
|
||||
# ECM and jammer pods
|
||||
(r"\bALQ-?\d+\b|ECM Pod|ECM|Jammer|U22/A|U22A", "ECM pod"),
|
||||
# Flares and dispensers (chaff/flares/countermeasures)
|
||||
(r"\bALE-?40\b|BOZ-107|Dispenser|Disperser|Countermeasure Dispenser|BOZ|ALE-40|SUU-?\d+|flares|Flare|LUU-2|Flare|Dispenser\(Empty\)", "Flares/Dispensers"),
|
||||
# Training rounds / captive
|
||||
(r"\bCATM|CAP-?9|TGM-?\d+|CATM", "Training/trg round"),
|
||||
(r"\bTGM-?\d+|TGM|CATM", "Training/trg round"),
|
||||
# Fuel tanks (various naming conventions)
|
||||
(r"\bFuel Tank\b|Fuel tank|Drop Tank|External[- ]?tank|Auxiliary Drop Tank|Sargent Fletcher Fuel Tank|RP35 Pylon Fuel Tank|RPL \d+|Cylindrical Tip Tank|Elliptic Tip Tank|\b\d+\s*(?:gal|gallons|liters|litres|L|lt)\b|1150L|1400L|2000L|3000L", "Fuel tank"),
|
||||
# Practice of captive or other small categories
|
||||
(r"\bMk-82 AIR Ballute|Ballute", "General purpose bomb"),
|
||||
# Misc / smoke / oil tanks / containers
|
||||
(r"\bSmoke\b|Smoke Generator|Smoke System|White Smoke|red colorant|yellow colorant|Color Oil Tank|White Oil Tank", "Misc"),
|
||||
# Pylons, containers and luggage
|
||||
(r"\bPYLON|Pylon|MPS-410|CLB4-PYLON|Luggage Container|Container", "Pylon"),
|
||||
# Guns and cannon mounts
|
||||
(r"\bDEFA-553|Browning|7.62mm|12.7mm|GPMG|Gun|Cannon", "Gun"),
|
||||
# Fallback guided bomb entries covered specifically
|
||||
(r"\bGBU-12|GBU-10|GBU-31|GBU-38", "Guided bomb"),
|
||||
]
|
||||
|
||||
# Default type when no pattern matches
|
||||
DEFAULT_TYPE = "unknown"
|
||||
|
||||
|
||||
def detect_type(item_name: str) -> str:
|
||||
name = item_name or ""
|
||||
# normalize
|
||||
s = name
|
||||
# 1) try mapping file exact match
|
||||
if MAP_PATH.exists():
|
||||
try:
|
||||
with MAP_PATH.open('r', encoding='utf-8') as mf:
|
||||
mapping = json.load(mf)
|
||||
except Exception:
|
||||
mapping = {}
|
||||
# exact name match (case-sensitive), then case-insensitive key match
|
||||
if name in mapping and mapping[name]:
|
||||
return mapping[name]
|
||||
# case-insensitive exact
|
||||
lower_map = {k.lower(): v for k, v in mapping.items() if v}
|
||||
if name.lower() in lower_map:
|
||||
return lower_map[name.lower()]
|
||||
# substring mapping: if a mapping key is contained in the name, use it
|
||||
for k, v in mapping.items():
|
||||
if not v:
|
||||
continue
|
||||
if k.lower() in name.lower():
|
||||
return v
|
||||
|
||||
for pattern, t in TYPE_MAP:
|
||||
if re.search(pattern, s, re.IGNORECASE):
|
||||
return t
|
||||
return DEFAULT_TYPE
|
||||
|
||||
|
||||
def process_db(db_path: Path):
|
||||
if not db_path.exists():
|
||||
print(f"Database file not found: {db_path}")
|
||||
return
|
||||
|
||||
backup_path = db_path.with_suffix(db_path.suffix + BACKUP_SUFFIX)
|
||||
shutil.copy2(db_path, backup_path)
|
||||
print(f"Created backup: {backup_path}")
|
||||
|
||||
with db_path.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
total_items = 0
|
||||
updated_items = 0
|
||||
type_counts = {}
|
||||
|
||||
# data is a dict of vehicles (aircraft or helicopter)
|
||||
for ac_name, ac in data.items():
|
||||
loadouts = ac.get("loadouts")
|
||||
if not isinstance(loadouts, list):
|
||||
continue
|
||||
for loadout in loadouts:
|
||||
items = loadout.get("items")
|
||||
if not isinstance(items, list):
|
||||
continue
|
||||
for item in items:
|
||||
total_items += 1
|
||||
name = item.get("name", "")
|
||||
t = detect_type(name)
|
||||
prev = item.get("type")
|
||||
if prev != t:
|
||||
item["type"] = t
|
||||
updated_items += 1
|
||||
type_counts[t] = type_counts.get(t, 0) + 1
|
||||
|
||||
# write back
|
||||
with db_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
|
||||
print(f"Processed {total_items} loadout items, updated {updated_items} entries for {db_path.name}.")
|
||||
print("Type counts:")
|
||||
for k, v in sorted(type_counts.items(), key=lambda x: -x[1]):
|
||||
print(f" {k}: {v}")
|
||||
|
||||
|
||||
def main():
|
||||
for p in DB_PATHS:
|
||||
process_db(p)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -92,7 +92,9 @@ for filename in filenames:
|
||||
src = tmp['payloads'].values()
|
||||
else:
|
||||
src = tmp['payloads']
|
||||
|
||||
|
||||
print(f"Processing {filename} with {len(src)} payloads, detected unit name {tmp['unitType']}")
|
||||
|
||||
names[tmp['unitType']] = []
|
||||
roles[tmp['unitType']] = {}
|
||||
payloads[tmp['unitType']] = {}
|
||||
@@ -129,9 +131,22 @@ for filename in filenames:
|
||||
with open('payloadRoles.json', 'w') as f:
|
||||
json.dump(roles, f, ensure_ascii = False, indent = 2)
|
||||
|
||||
with open('../unitPayloads.lua', 'w') as f:
|
||||
with open('unitPayloads.lua', 'w') as f:
|
||||
f.write("Olympus.unitPayloads = " + dump_lua(payloads))
|
||||
|
||||
|
||||
|
||||
# Iterate over the payloads and accumulate the pylon data
|
||||
pylon_usage = {}
|
||||
for unitType, unitPayloads in payloads.items():
|
||||
pylon_usage[unitType] = {}
|
||||
for payloadName, pylons in unitPayloads.items():
|
||||
for pylonID, pylonData in pylons.items():
|
||||
# Keep track of what CLSIDs are used on each pylon
|
||||
clsid = pylonData['CLSID']
|
||||
if pylonID not in pylon_usage[unitType]:
|
||||
pylon_usage[unitType][pylonID] = []
|
||||
if clsid not in pylon_usage[unitType][pylonID]:
|
||||
pylon_usage[unitType][pylonID].append(clsid)
|
||||
|
||||
# Save the pylon usage data to a JSON file
|
||||
with open('pylonUsage.json', 'w') as f:
|
||||
json.dump(pylon_usage, f, ensure_ascii=False, indent=2)
|
||||
65
scripts/python/loadout_type_map.json
Normal file
65
scripts/python/loadout_type_map.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"2 x FAB-250": "General purpose bomb",
|
||||
"2 x FAB-500": "General purpose bomb",
|
||||
"6 x AGM-86D on MER": "A/G missile",
|
||||
"8 x AGM-86D": "A/G missile",
|
||||
"<CLEAN>": "Empty",
|
||||
"AGM-114K * 2": "A/G missile",
|
||||
"AGM-114K Hellfire": "A/G missile",
|
||||
"AGM-154C - JSOW Unitary BROACH": "Guided bomb",
|
||||
"AGM-88C HARM - High Speed Anti-Radiation Missile": "A/G missile",
|
||||
"AIM-120B AMRAAM - Active Radar AAM": "A/A missile",
|
||||
"AIM-120C AMRAAM - Active Radar AAM": "A/A missile",
|
||||
"AIM-7E Sparrow Semi-Active Radar": "A/A missile",
|
||||
"AIM-7E-2 Sparrow Semi-Active Radar": "A/A missile",
|
||||
"AIM-7F": "A/A missile",
|
||||
"AIM-7M": "A/A missile",
|
||||
"AIM-7M Sparrow Semi-Active Radar": "A/A missile",
|
||||
"AIM-7MH Sparrow Semi-Active Radar": "A/A missile",
|
||||
"AKD-10": "A/G missile",
|
||||
"ALARM": "A/G missile",
|
||||
"AN-M3 - 2*Browning Machine Guns 12.7mm": "Gun",
|
||||
"AN/ASQ-213 HTS - HARM Targeting System": "Targeting pod",
|
||||
"APU-13U-2 with R-3R (AA-2 Atoll-C) - Semi Active AAM": "A/A missile",
|
||||
"APU-13U-2 with R-3S (AA-2 Atoll-B) - IR AAM": "A/A missile",
|
||||
"BAP-100 x 18": "Unguided rocket",
|
||||
"BLG-66-AC Belouga": "Cluster bomb",
|
||||
"C-701IR": "A/G missile",
|
||||
"C-701T": "A/G missile",
|
||||
"C-802AK": "A/G missile",
|
||||
"CLB4-PYLON-SAMP250HD": "Pylon",
|
||||
"CLB4-PYLON-SAMP400LD": "Pylon",
|
||||
"CM802AKG (DIS)": "A/G missile",
|
||||
"DEFA-553 - 30mm Revolver Cannon": "Gun",
|
||||
"ETHER": "Misc",
|
||||
"FAB-100SV": "General purpose bomb",
|
||||
"GB-6": "General purpose bomb",
|
||||
"GB-6-HE": "General purpose bomb",
|
||||
"K-13A": "A/A missile",
|
||||
"KD-20": "A/G missile",
|
||||
"KD-63": "A/G missile",
|
||||
"KG-600": "A/G missile",
|
||||
"Kh-22 (AS-4 Kitchen) - 1000kg, AShM, IN & Act/Pas Rdr": "A/G missile",
|
||||
"Kh-25MPU (Updated AS-12 Kegler) - 320kg, ARM, IN & Pas Rdr": "A/G missile",
|
||||
"Kh-58U (AS-11 Kilter) - 640kg, ARM, IN & Pas Rdr": "A/G missile",
|
||||
"Kh-66 Grom (21) - AGM, radar guided APU-68": "A/G missile",
|
||||
"LD-10": "Fuel tank",
|
||||
"LD-10 x 2": "Fuel tank",
|
||||
"LS-6-100 Dual": "Guided bomb",
|
||||
"LS-6-250 Dual": "Guided bomb",
|
||||
"LS-6-500": "Guided bomb",
|
||||
"Luggage Container": "Container",
|
||||
"MPRL - 4 x AGM-154C - JSOW Unitary BROACH": "Guided bomb",
|
||||
"MPS-410": "Pylon",
|
||||
"Mk-82AIR": "General purpose bomb",
|
||||
"PK-3 - 7.62mm GPMG": "Gun",
|
||||
"PL-5EII": "A/A missile",
|
||||
"PL-8B": "A/A missile",
|
||||
"R550 Magic 1 IR AAM": "A/A missile",
|
||||
"R550 Magic 2 IR AAM": "A/A missile",
|
||||
"S530D": "A/A missile",
|
||||
"S530F": "A/A missile",
|
||||
"TYPE-200A": "Flares/Dispensers",
|
||||
"TYPE-200A Dual": "Flares/Dispensers",
|
||||
"{6C0D552F-570B-42ff-9F6D-F10D9C1D4E1C}": "Misc"
|
||||
}
|
||||
@@ -3773,6 +3773,76 @@
|
||||
"1": 29
|
||||
}
|
||||
},
|
||||
"F4U-1D": {
|
||||
"Drop tank 175 US gal.": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 17
|
||||
},
|
||||
"HVAR x 8": {
|
||||
"1": 31
|
||||
},
|
||||
"M-64 bomb x 2, HVAR x 8": {
|
||||
"1": 31
|
||||
},
|
||||
"M-64 bomb x 3, HVAR x 6": {
|
||||
"1": 31
|
||||
},
|
||||
"M-64 bomb x 2, HVAR x 4": {
|
||||
"1": 32
|
||||
},
|
||||
"M-64 bomb, M-65 bomb x 2": {
|
||||
"1": 32
|
||||
},
|
||||
"Tiny Tim x 2, HVAR x 4": {
|
||||
"1": 32
|
||||
},
|
||||
"Bat Bomb": {
|
||||
"1": 30
|
||||
},
|
||||
"Tiny Tim x 2": {
|
||||
"1": 30
|
||||
},
|
||||
"Bat Bomb, HVAR x 8": {
|
||||
"1": 30
|
||||
}
|
||||
},
|
||||
"F4U-1D_CW": {
|
||||
"Drop tank 175 US gal.": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 17
|
||||
},
|
||||
"HVAR x 8": {
|
||||
"1": 31
|
||||
},
|
||||
"M-64 bomb x 2, HVAR x 8": {
|
||||
"1": 31
|
||||
},
|
||||
"M-64 bomb x 3, HVAR x 6": {
|
||||
"1": 31
|
||||
},
|
||||
"M-64 bomb x 2, HVAR x 4": {
|
||||
"1": 32
|
||||
},
|
||||
"M-64 bomb, M-65 bomb x 2": {
|
||||
"1": 32
|
||||
},
|
||||
"Tiny Tim x 2, HVAR x 4": {
|
||||
"1": 32
|
||||
},
|
||||
"Bat Bomb": {
|
||||
"1": 30
|
||||
},
|
||||
"Tiny Tim x 2": {
|
||||
"1": 30
|
||||
},
|
||||
"Bat Bomb, HVAR x 8": {
|
||||
"1": 30
|
||||
}
|
||||
},
|
||||
"F/A-18A": {
|
||||
"GBU-16*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3": {
|
||||
"1": 33
|
||||
@@ -4725,7 +4795,219 @@
|
||||
},
|
||||
"AEROBATIC": {}
|
||||
},
|
||||
"MiG-29 Fulcrum": {
|
||||
"2 * R-27T, 4 * R-73": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27R, 4 * R-73": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27T, 2 * R-73": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27R, 2 * R-73": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"6 * R-73": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27T, 4 * R-60M": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27R, 4 * R-60M": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27T, 2 * R-60M": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27R, 2 * R-60M": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"6 * R-60": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"4 * S-24, 2 * R-73": {
|
||||
"1": 34,
|
||||
"2": 30,
|
||||
"3": 31,
|
||||
"4": 32
|
||||
},
|
||||
"4 * S-24, 2 * R-60M ": {
|
||||
"1": 34,
|
||||
"2": 30,
|
||||
"3": 31,
|
||||
"4": 32
|
||||
},
|
||||
"4 * S-24": {
|
||||
"1": 34,
|
||||
"2": 30,
|
||||
"3": 31,
|
||||
"4": 32
|
||||
},
|
||||
"80 * S-8OFP": {
|
||||
"1": 31,
|
||||
"2": 32
|
||||
},
|
||||
"80 * S-8OFP, 2 * R-60M": {
|
||||
"1": 31,
|
||||
"2": 32
|
||||
},
|
||||
"80 * S-8KOM, 2 * R-73": {
|
||||
"1": 31,
|
||||
"2": 32
|
||||
},
|
||||
"4 * BetAB-500, 2 * R-60": {
|
||||
"1": 34,
|
||||
"2": 32
|
||||
},
|
||||
"4 * BetAB-500, 2 * R-73": {
|
||||
"1": 34,
|
||||
"2": 32
|
||||
},
|
||||
"4 * BetAB-500": {
|
||||
"1": 34,
|
||||
"2": 32
|
||||
},
|
||||
"4 * KMGU": {
|
||||
"1": 31
|
||||
},
|
||||
"4 * KMGU AO-2.5RT, 2 * R-60M": {
|
||||
"1": 31
|
||||
},
|
||||
"4 * KMGU PTAB-2.5KO, 2 * R-73": {
|
||||
"1": 31
|
||||
},
|
||||
"2 * R-27T": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27R": {
|
||||
"1": 11,
|
||||
"2": 18,
|
||||
"3": 19,
|
||||
"4": 10
|
||||
},
|
||||
"4 * RBK-250 PTAB-2.5M, 2 * R-73": {
|
||||
"1": 31
|
||||
},
|
||||
"4 * RBK-250 AO-1SCh, 2 * R-73": {
|
||||
"1": 31
|
||||
},
|
||||
"4 * RBK-500 PTAB-1M, 2 * R-73": {
|
||||
"1": 31
|
||||
},
|
||||
"4 * RBK-500 PTAB-10, 2 * R-73": {
|
||||
"1": 31
|
||||
},
|
||||
"4 * FAB-250M-62, 2 * R-73": {
|
||||
"1": 31,
|
||||
"2": 32
|
||||
},
|
||||
"4 * FAB-500M-62, 2 * R-73": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 30
|
||||
},
|
||||
"80 * S-8OFP, 2 * R-73": {
|
||||
"1": 31
|
||||
},
|
||||
"4 * BetAB-500ShP, 2 * R-73": {
|
||||
"1": 34
|
||||
},
|
||||
"80 * S-8TsM, 2 * R-73": {
|
||||
"1": 16
|
||||
},
|
||||
"6 * R-60M": {
|
||||
"1": 19
|
||||
},
|
||||
"2 * R-27ER, 4 * R-73": {
|
||||
"1": 19,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27ET, 4 * R-73": {
|
||||
"1": 19,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27ER, 4 * R-60M": {
|
||||
"1": 19,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27ER": {
|
||||
"1": 19,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27ET": {
|
||||
"1": 19,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 10
|
||||
},
|
||||
"2 * R-27ER, 4 * R-73, Fuel": {
|
||||
"1": 18
|
||||
},
|
||||
"2 * R-27ET, 4 * R-73, fuel": {
|
||||
"1": 18
|
||||
},
|
||||
"6 * R-73, Fuel": {
|
||||
"1": 18
|
||||
},
|
||||
"6 * R-60M, Fuel": {
|
||||
"1": 18
|
||||
},
|
||||
"2 * R-73, Fuel": {
|
||||
"1": 18
|
||||
}
|
||||
},
|
||||
"Mirage-F1B": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -4781,6 +5063,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1BD": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -4830,6 +5121,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1BE": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -4909,6 +5209,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1BQ": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -4958,6 +5267,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1C-200": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5013,6 +5331,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1C": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5068,6 +5395,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1CE": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5147,6 +5483,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1CG": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*AIM-9 JULI, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5208,6 +5553,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1CH": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5263,6 +5617,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1CJ": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5318,6 +5681,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1CK": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5373,6 +5745,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1CR": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I": {
|
||||
"1": 10
|
||||
},
|
||||
@@ -5410,6 +5791,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1CT": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5459,6 +5849,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1CZ": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5514,6 +5913,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1DDA": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5563,6 +5971,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1ED": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic II, 2*S530F, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5612,6 +6029,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1EDA": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5661,6 +6087,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1EE": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5746,6 +6181,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1EH": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5801,6 +6245,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1EQ": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5850,6 +6303,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1JA": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*R550 Magic I, 2*Python III, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5896,6 +6358,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1M-CE": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -5969,6 +6440,15 @@
|
||||
}
|
||||
},
|
||||
"Mirage-F1M-EE": {
|
||||
"Clean": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": 18,
|
||||
"4": 19,
|
||||
"5": 31,
|
||||
"6": 32,
|
||||
"7": 34
|
||||
},
|
||||
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
@@ -6213,6 +6693,125 @@
|
||||
"1": 18
|
||||
}
|
||||
},
|
||||
"Mi-28N": {
|
||||
"2xFAB-250": {
|
||||
"1": 32
|
||||
},
|
||||
"4xFuel tank": {
|
||||
"1": 15
|
||||
},
|
||||
"80xS-8": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"4xKMGU AP": {
|
||||
"1": 32
|
||||
},
|
||||
"4xUPK-23": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"16x9M114, 2xKMGU AT": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"4xFAB-500": {
|
||||
"1": 32
|
||||
},
|
||||
"16x9M114, 2xFAB-500": {
|
||||
"1": 32
|
||||
},
|
||||
"40xS-8": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"40xS-8 TsM": {
|
||||
"1": 16
|
||||
},
|
||||
"2xKMGU AP": {
|
||||
"1": 32
|
||||
},
|
||||
"2xUPK-23": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"16x9M114, 2xUPK-23": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"2xFAB-500": {
|
||||
"1": 32
|
||||
},
|
||||
"16x9M114, 40xS-8": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18,
|
||||
"4": 30
|
||||
},
|
||||
"16x9M114": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"20xS-13": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"16x9M114, 2xKMGU AP": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"4xFAB-250": {
|
||||
"1": 32
|
||||
},
|
||||
"4xKMGU AT": {
|
||||
"1": 32
|
||||
},
|
||||
"16x9M114, 40xS-8 TsM": {
|
||||
"1": 16
|
||||
},
|
||||
"80xS-8 TsM": {
|
||||
"1": 16
|
||||
},
|
||||
"2xKMGU AT": {
|
||||
"1": 32
|
||||
},
|
||||
"9x9M114": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"2xFuel tank": {
|
||||
"1": 15
|
||||
},
|
||||
"10xS-13": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"2xFAB-250, 16x9M114": {
|
||||
"1": 32
|
||||
},
|
||||
"16x9M114, 10xS-13": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18,
|
||||
"4": 30
|
||||
}
|
||||
},
|
||||
"Tu-95MS": {
|
||||
"Kh-65*6": {
|
||||
"1": 33
|
||||
}
|
||||
},
|
||||
"B-1B": {
|
||||
"Mk-82*84": {
|
||||
"1": 34,
|
||||
@@ -7401,120 +8000,6 @@
|
||||
}
|
||||
},
|
||||
"Mi-26": {},
|
||||
"Mi-28N": {
|
||||
"2xFAB-250": {
|
||||
"1": 32
|
||||
},
|
||||
"4xFuel tank": {
|
||||
"1": 15
|
||||
},
|
||||
"80xS-8": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"4xKMGU AP": {
|
||||
"1": 32
|
||||
},
|
||||
"4xUPK-23": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"16x9M114, 2xKMGU AT": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"4xFAB-500": {
|
||||
"1": 32
|
||||
},
|
||||
"16x9M114, 2xFAB-500": {
|
||||
"1": 32
|
||||
},
|
||||
"40xS-8": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"40xS-8 TsM": {
|
||||
"1": 16
|
||||
},
|
||||
"2xKMGU AP": {
|
||||
"1": 32
|
||||
},
|
||||
"2xUPK-23": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"16x9M114, 2xUPK-23": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"2xFAB-500": {
|
||||
"1": 32
|
||||
},
|
||||
"16x9M114, 40xS-8": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18,
|
||||
"4": 30
|
||||
},
|
||||
"16x9M114": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"20xS-13": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"16x9M114, 2xKMGU AP": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"4xFAB-250": {
|
||||
"1": 32
|
||||
},
|
||||
"4xKMGU AT": {
|
||||
"1": 32
|
||||
},
|
||||
"16x9M114, 40xS-8 TsM": {
|
||||
"1": 16
|
||||
},
|
||||
"80xS-8 TsM": {
|
||||
"1": 16
|
||||
},
|
||||
"2xKMGU AT": {
|
||||
"1": 32
|
||||
},
|
||||
"9x9M114": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"2xFuel tank": {
|
||||
"1": 15
|
||||
},
|
||||
"10xS-13": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18
|
||||
},
|
||||
"2xFAB-250, 16x9M114": {
|
||||
"1": 32
|
||||
},
|
||||
"16x9M114, 10xS-13": {
|
||||
"1": 31,
|
||||
"2": 32,
|
||||
"3": 18,
|
||||
"4": 30
|
||||
}
|
||||
},
|
||||
"Mi-8MT": {
|
||||
"4 x B8": {
|
||||
"1": 32
|
||||
@@ -8145,7 +8630,7 @@
|
||||
"UB-32*2,Fuel*3": {
|
||||
"1": 32
|
||||
},
|
||||
"Kh-59M*2,R-60M*2,Fuel": {
|
||||
"Kh-59M*2,R-60M*2": {
|
||||
"1": 33
|
||||
},
|
||||
"S-25*4": {
|
||||
@@ -8934,11 +9419,6 @@
|
||||
"2": 34
|
||||
}
|
||||
},
|
||||
"Tu-95MS": {
|
||||
"Kh-65*6": {
|
||||
"1": 33
|
||||
}
|
||||
},
|
||||
"UH-1H": {
|
||||
"M134 Minigun*2, XM158*2": {
|
||||
"1": 32,
|
||||
|
||||
7210
scripts/python/pylonUsage.json
Normal file
7210
scripts/python/pylonUsage.json
Normal file
File diff suppressed because it is too large
Load Diff
0
scripts/python/pylonUsage.lua
Normal file
0
scripts/python/pylonUsage.lua
Normal file
File diff suppressed because it is too large
Load Diff
1
scripts/python/unknown_loadout_items.txt
Normal file
1
scripts/python/unknown_loadout_items.txt
Normal file
@@ -0,0 +1 @@
|
||||
AB 250-2 - 144 x SD-2, 250kg CBU with HE submunitions
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "v2.0.3"
|
||||
"version": "v2.0.4"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user