17 Commits

Author SHA1 Message Date
Pax1601
b42280133c Completed custom weapon wizard modal 2025-10-26 16:34:27 +01:00
Pax1601
94d0b4d10e Merge branch 'release-candidate' into weapon-wizard 2025-10-25 15:17:18 +02:00
Pax1601
057603f926 First implementation of weapon wizard 2025-10-23 18:07:55 +02:00
Pax1601
bfd11c49af Merge branch 'release-candidate' of https://github.com/Pax1601/DCSOlympus into release-candidate 2025-10-23 18:07:35 +02:00
Pax1601
2a9723b932 Add image overlay import modal and menu option
Introduces ImageOverlayModal for importing image overlays with user-specified corner coordinates. Adds a menu item to trigger the modal and integrates it into the main UI component. Also updates OlNumberInput to support an internalClassName prop for styling flexibility.
2025-10-23 18:06:29 +02:00
Pax1601
f565b9ee6e Add type annotations and key conversions in Map class
Improves type safety by adding explicit type annotations to method parameters and callback functions in the Map class. Updates key handling for object properties to ensure correct types, particularly when interacting with ContextActions, MapOptions, MapHiddenTypes, and destination preview markers.
2025-10-21 17:34:20 +02:00
Pax1601
504c0a0ed9 fix: Map now works with capitalized map names (thanks for creating this need Wirts :P) 2025-10-15 18:50:40 +02:00
Pax1601
c77173f7c9 Enable AAA capability for infantry and fix map layer key case
Set 'canAAA' to true for several infantry units in groundunitdatabase.json, allowing them to engage air targets. Updated map.ts to handle map layer keys in a case-insensitive manner, preventing issues with mismatched key casing.
2025-10-13 22:50:36 +02:00
Pax1601
73af60d91b Updated unit databases and new spawn loadout system 2025-10-12 15:11:55 +02:00
Pax1601
31d7fb6051 Update mist.lua 2025-09-28 12:10:20 +02:00
Pax1601
def15f5565 Merge branch 'python-api' into release-candidate 2025-09-27 18:08:13 +02:00
Pax1601
a257afca4b Add customString and customInteger to Unit data model
Introduced customString and customInteger fields to the Unit class in both backend (C++) and frontend (TypeScript/React). Updated data indexes, interfaces, and API handling to support setting and retrieving these custom fields. Also added UI elements in the unit control menu to display and handle these new properties.
2025-09-27 18:07:37 +02:00
Pax1601
dca8f9189f Merge branch 'python-api' into release-candidate 2025-09-11 21:47:29 +02:00
Pax1601
3eef91fb24 Add cargo weight and draw argument support
Introduces cargo weight and draw argument properties to units across backend, frontend, and Python API. Adds related commands, data extraction, and registration logic, enabling setting and reading of cargo weight and custom draw arguments for units. Includes new API examples and updates to interfaces, data types, and Lua backend for full feature integration.
2025-09-11 21:47:11 +02:00
Pax1601
73a7ea74f3 feat: Added threshold to unit movement 2025-09-09 18:24:53 +02:00
Pax1601
4e6701ff01 Refactor API callbacks and improve example scripts
Moved register_on_update_callback in api.py for better code organization. Fixed initialization of units_to_delete and corrected simulate_fire_fight usage in example_disembarked_infantry.py. In example_voice_control.py, added cleanup of generated audio files and fixed callback parameter naming for clarity.
2025-08-08 13:14:59 +02:00
Pax1601
5fa1a26843 Add async callbacks and Kronos integration to API
Introduces async callback support for command execution in spawn methods, adds registration/unregistration for update and startup callbacks, and improves logging and signal handling. Adds a new Kronos module and main entry point for initializing and running the API with Kronos integration. Refactors example scripts and updates VSCode launch configurations for new entry points.
2025-08-08 11:06:53 +02:00
60 changed files with 148860 additions and 43527 deletions

View File

@@ -23,6 +23,8 @@ public:
virtual void setRacetrackAnchor(Coords newValue); virtual void setRacetrackAnchor(Coords newValue);
virtual void setRacetrackBearing(double newValue); virtual void setRacetrackBearing(double newValue);
virtual void setCargoWeight(double newValue);
protected: protected:
virtual void AIloop(); virtual void AIloop();
}; };

View File

@@ -539,3 +539,43 @@ private:
const unsigned int spotID; const unsigned int spotID;
const Coords destination; const Coords destination;
}; };
/* Set cargo weight */
class SetCargoWeight : public Command
{
public:
SetCargoWeight(unsigned int ID, double weight, function<void(void)> callback = []() {}) :
Command(callback),
ID(ID),
weight(weight)
{
priority = CommandPriority::LOW;
};
virtual string getString();
virtual unsigned int getLoad() { return 5; }
private:
const unsigned int ID;
const double weight;
};
/* Register draw argument */
class RegisterDrawArgument : public Command
{
public:
RegisterDrawArgument(unsigned int ID, unsigned int argument, bool active, function<void(void)> callback = []() {}) :
Command(callback),
ID(ID),
argument(argument),
active(active)
{
priority = CommandPriority::LOW;
};
virtual string getString();
virtual unsigned int getLoad() { return 5; }
private:
const unsigned int ID;
const unsigned int argument;
const bool active;
};

View File

@@ -70,6 +70,10 @@ namespace DataIndex {
aimMethodRange, aimMethodRange,
acquisitionRange, acquisitionRange,
airborne, airborne,
cargoWeight,
drawArguments,
customString,
customInteger,
lastIndex, lastIndex,
endOfData = 255 endOfData = 255
}; };
@@ -159,6 +163,11 @@ namespace DataTypes {
unsigned int ID = 0; unsigned int ID = 0;
unsigned char detectionMethod = 0; unsigned char detectionMethod = 0;
}; };
struct DrawArgument {
unsigned int argument = 0;
double value = 0.0;
};
} }
#pragma pack(pop) #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::GeneralSettings& lhs, const DataTypes::GeneralSettings& rhs);
bool operator==(const DataTypes::Ammo& lhs, const DataTypes::Ammo& 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::Contact& lhs, const DataTypes::Contact& rhs);
bool operator==(const DataTypes::DrawArgument& lhs, const DataTypes::DrawArgument& rhs);
struct SpawnOptions { struct SpawnOptions {
string unitType; string unitType;
@@ -175,6 +185,7 @@ struct SpawnOptions {
string skill; string skill;
string liveryID; string liveryID;
double heading; double heading;
string payload;
}; };
struct CloneOptions { struct CloneOptions {

View File

@@ -19,6 +19,7 @@ public:
return true; return true;
} }
} }
return false;
} }
void setFrameRate(double newFrameRate) { frameRate = newFrameRate; } void setFrameRate(double newFrameRate) { frameRate = newFrameRate; }

View File

@@ -130,9 +130,13 @@ public:
virtual void setAcquisitionRange(double newValue) { updateValue(acquisitionRange, newValue, DataIndex::acquisitionRange); } virtual void setAcquisitionRange(double newValue) { updateValue(acquisitionRange, newValue, DataIndex::acquisitionRange); }
virtual void setRadarState(bool newValue) { updateValue(radarState, newValue, DataIndex::radarState); } virtual void setRadarState(bool newValue) { updateValue(radarState, newValue, DataIndex::radarState); }
virtual void setAirborne(bool newValue) { updateValue(airborne, newValue, DataIndex::airborne); } 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 **********/ /********** Getters **********/
virtual string getCategory() { return category; }; virtual string getCategory() { return category; }
virtual bool getAlive() { return alive; } virtual bool getAlive() { return alive; }
virtual unsigned char getAlarmState() { return alarmState; } virtual unsigned char getAlarmState() { return alarmState; }
virtual bool getHuman() { return human; } virtual bool getHuman() { return human; }
@@ -197,6 +201,10 @@ public:
virtual double getAcquisitionRange() { return acquisitionRange; } virtual double getAcquisitionRange() { return acquisitionRange; }
virtual bool getRadarState() { return radarState; } virtual bool getRadarState() { return radarState; }
virtual bool getAirborne() { return airborne; } 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: protected:
unsigned int ID; unsigned int ID;
@@ -267,6 +275,11 @@ protected:
double aimMethodRange = 0; double aimMethodRange = 0;
double acquisitionRange = 0; double acquisitionRange = 0;
bool airborne = false; bool airborne = false;
double cargoWeight = 0;
vector<DataTypes::DrawArgument> drawArguments;
string customString = "";
unsigned long customInteger = 0;
/********** Other **********/ /********** Other **********/
unsigned int taskCheckCounter = 0; unsigned int taskCheckCounter = 0;

View File

@@ -429,3 +429,13 @@ void AirUnit::setRacetrackBearing(double newRacetrackBearing) {
triggerUpdate(DataIndex::racetrackBearing); triggerUpdate(DataIndex::racetrackBearing);
} }
} }
void AirUnit::setCargoWeight(double newCargoWeight) {
if (cargoWeight != newCargoWeight) {
cargoWeight = newCargoWeight;
triggerUpdate(DataIndex::cargoWeight);
Command* command = dynamic_cast<Command*>(new SetCargoWeight(this->ID, cargoWeight));
scheduler->appendCommand(command);
}
}

View File

@@ -102,6 +102,7 @@ string SpawnAircrafts::getString()
<< "alt = " << spawnOptions[i].location.alt << ", " << "alt = " << spawnOptions[i].location.alt << ", "
<< "heading = " << spawnOptions[i].heading << ", " << "heading = " << spawnOptions[i].heading << ", "
<< "loadout = \"" << spawnOptions[i].loadout << "\"" << ", " << "loadout = \"" << spawnOptions[i].loadout << "\"" << ", "
<< "payload = " << spawnOptions[i].payload << ", "
<< "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", " << "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", "
<< "skill = \"" << spawnOptions[i].skill << "\"" << "}, "; << "skill = \"" << spawnOptions[i].skill << "\"" << "}, ";
} }
@@ -132,6 +133,7 @@ string SpawnHelicopters::getString()
<< "alt = " << spawnOptions[i].location.alt << ", " << "alt = " << spawnOptions[i].location.alt << ", "
<< "heading = " << spawnOptions[i].heading << ", " << "heading = " << spawnOptions[i].heading << ", "
<< "loadout = \"" << spawnOptions[i].loadout << "\"" << ", " << "loadout = \"" << spawnOptions[i].loadout << "\"" << ", "
<< "payload = " << spawnOptions[i].payload << ", "
<< "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", " << "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", "
<< "skill = \"" << spawnOptions[i].skill << "\"" << "}, "; << "skill = \"" << spawnOptions[i].skill << "\"" << "}, ";
} }
@@ -319,3 +321,26 @@ string DeleteSpot::getString()
<< spotID; << spotID;
return commandSS.str(); return commandSS.str();
} }
/* SetCargoWeight command */
string SetCargoWeight::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
commandSS << "Olympus.setCargoWeight, "
<< ID << ", "
<< weight;
return commandSS.str();
}
/* RegisterDrawArgument command */
string RegisterDrawArgument::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
commandSS << "Olympus.registerDrawArgument, "
<< ID << ", "
<< argument << ", "
<< active;
return commandSS.str();
}

View File

@@ -12,19 +12,24 @@ bool operator==(const DataTypes::Radio& lhs, const DataTypes::Radio& rhs)
bool operator==(const DataTypes::GeneralSettings& lhs, const DataTypes::GeneralSettings& 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; lhs.prohibitAirWpn == rhs.prohibitAirWpn && lhs.prohibitJettison == rhs.prohibitJettison;
} }
bool operator==(const DataTypes::Ammo& lhs, const DataTypes::Ammo& rhs) 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; 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) bool operator==(const DataTypes::Contact& lhs, const DataTypes::Contact& rhs)
{ {
return lhs.detectionMethod == rhs.detectionMethod && lhs.ID == rhs.ID; return lhs.detectionMethod == rhs.detectionMethod && lhs.ID == rhs.ID;
} }

View File

@@ -168,6 +168,12 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
string WP = to_string(i); string WP = to_string(i);
double lat = path[i][L"lat"].as_double(); double lat = path[i][L"lat"].as_double();
double lng = path[i][L"lng"].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; Coords dest; dest.lat = lat; dest.lng = lng;
newPath.push_back(dest); newPath.push_back(dest);
} }
@@ -217,8 +223,12 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
string liveryID = to_string(unit[L"liveryID"]); string liveryID = to_string(unit[L"liveryID"]);
string skill = to_string(unit[L"skill"]); string skill = to_string(unit[L"skill"]);
spawnOptions.push_back({unitType, location, loadout, skill, liveryID, heading}); string payload = "nil";
log(username + " spawned a " + coalition + " " + unitType , true); 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) if (key.compare("spawnAircrafts") == 0)
@@ -252,7 +262,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
string liveryID = to_string(unit[L"liveryID"]); string liveryID = to_string(unit[L"liveryID"]);
string skill = to_string(unit[L"skill"]); 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); log(username + " spawned a " + coalition + " " + unitType, true);
} }
@@ -427,7 +437,8 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
unsigned char alarmState = value[L"alarmState"].as_number().to_uint32(); unsigned char alarmState = value[L"alarmState"].as_number().to_uint32();
unit->setAlarmState(alarmState); unit->setAlarmState(alarmState);
log(username + " set unit " + unit->getUnitName() + "(" + unit->getName() + ") alarm state to " + to_string(alarmState), true); 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."); log("Error while setting setAlarmState. Unit does not exist.");
} }
} }
@@ -581,7 +592,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
Unit* unit = unitsManager->getGroupLeader(ID); Unit* unit = unitsManager->getGroupLeader(ID);
if (unit != nullptr) { if (unit != nullptr) {
unit->setOnOff(onOff); 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);
} }
} }
/************************/ /************************/
@@ -821,6 +832,53 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
unitsManager->loadDatabases(); 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 else
{ {
log("Unknown command: " + key); log("Unknown command: " + key);

View File

@@ -128,6 +128,20 @@ void Unit::update(json::value json, double dt)
setAmmo(ammo); 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")) { if (json.has_object_field(L"contacts")) {
vector<DataTypes::Contact> contacts; vector<DataTypes::Contact> contacts;
for (auto const& el : json[L"contacts"].as_object()) { for (auto const& el : json[L"contacts"].as_object()) {
@@ -330,6 +344,10 @@ void Unit::getData(stringstream& ss, unsigned long long time)
case DataIndex::aimMethodRange: appendNumeric(ss, datumIndex, aimMethodRange); break; case DataIndex::aimMethodRange: appendNumeric(ss, datumIndex, aimMethodRange); break;
case DataIndex::acquisitionRange: appendNumeric(ss, datumIndex, acquisitionRange); break; case DataIndex::acquisitionRange: appendNumeric(ss, datumIndex, acquisitionRange); break;
case DataIndex::airborne: appendNumeric(ss, datumIndex, airborne); 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;
} }
} }
} }
@@ -701,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) void Unit::setDesiredSpeed(double newDesiredSpeed)
{ {
if (desiredSpeed != newDesiredSpeed) { if (desiredSpeed != newDesiredSpeed) {
@@ -767,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) bool Unit::isDestinationReached(double threshold)
{ {
if (activeDestination != NULL) if (activeDestination != NULL)
@@ -776,7 +813,7 @@ bool Unit::isDestinationReached(double threshold)
{ {
double dist = 0; double dist = 0;
Geodesic::WGS84().Inverse(p->getPosition().lat, p->getPosition().lng, activeDestination.lat, activeDestination.lng, dist); 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"); log(unitName + " destination reached");
return true; return true;

View File

@@ -6,6 +6,7 @@ struct Coords {
double lat = 0; double lat = 0;
double lng = 0; double lng = 0;
double alt = 0; double alt = 0;
double threshold = 0; // used for proximity checks only, not part of the actual coordinates
}; };
struct Offset { struct Offset {

View File

@@ -64,9 +64,9 @@ std::string random_string(size_t length)
return str; 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 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 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; } 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

View File

@@ -1,12 +1,6 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{
"label": "check-setup",
"type": "shell",
"command": "cd .. ; ./check_setup.bat",
"isBackground": false
},
{ {
"type": "npm", "type": "npm",
"script": "dev", "script": "dev",

View File

@@ -360,6 +360,7 @@ export enum OlympusState {
MEASURE = "Measure", MEASURE = "Measure",
TRAINING = "Training", TRAINING = "Training",
ADMIN = "Admin", ADMIN = "Admin",
IMPORT_IMAGE_OVERLAY = "Import image overlay"
} }
export const NO_SUBSTATE = "No substate"; export const NO_SUBSTATE = "No substate";
@@ -398,6 +399,7 @@ export enum SpawnSubState {
NO_SUBSTATE = "No substate", NO_SUBSTATE = "No substate",
SPAWN_UNIT = "Unit", SPAWN_UNIT = "Unit",
SPAWN_EFFECT = "Effect", SPAWN_EFFECT = "Effect",
LOADOUT_WIZARD = "Loadout wizard"
} }
export enum OptionsSubstate { export enum OptionsSubstate {
@@ -547,6 +549,10 @@ export enum DataIndexes {
aimMethodRange, aimMethodRange,
acquisitionRange, acquisitionRange,
airborne, airborne,
cargoWeight,
drawingArguments,
customString,
customInteger,
endOfData = 255, endOfData = 255,
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,393 +2,405 @@ import { LatLng } from "leaflet";
import { AudioOptions, Coalition, MapOptions } from "./types/types"; import { AudioOptions, Coalition, MapOptions } from "./types/types";
export interface OlympusConfig { export interface OlympusConfig {
/* Set by user */ /* Set by user */
frontend: { frontend: {
port: number; port: number;
elevationProvider: { elevationProvider: {
provider: string; provider: string;
username: string | null; username: string | null;
password: string | null; password: string | null;
}; };
mapLayers: { mapLayers: {
[key: string]: { [key: string]: {
urlTemplate: string; urlTemplate: string;
minZoom: number; minZoom: number;
maxZoom: number; maxZoom: number;
attribution?: string; attribution?: string;
}; };
}; };
mapMirrors: { mapMirrors: {
[key: string]: string; [key: string]: string;
};
/* New with v2.0.0 */
customAuthHeaders?: {
enabled: boolean;
username: string;
group: string;
};
autoconnectWhenLocal?: boolean;
}; };
/* New with v2.0.0 */ /* New with v2.0.0 */
customAuthHeaders?: { audio?: {
enabled: boolean; SRSPort: number;
username: string; WSPort?: number;
group: string; WSEndpoint?: string;
}; };
autoconnectWhenLocal?: boolean; controllers?: [{ type: string; coalition: Coalition; frequency: number; modulation: number; callsign: string }];
}; profiles?: { [key: string]: ProfileOptions };
/* 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 };
/* Set by server */ /* Set by server */
local?: boolean; local?: boolean;
authentication?: { authentication?: {
// Only sent when in localhost mode for autologin // Only sent when in localhost mode for autologin
gameMasterPassword: string; gameMasterPassword: string;
blueCommanderPassword: string; blueCommanderPassword: string;
redCommanderPassword: string; redCommanderPassword: string;
}; };
} }
export interface SessionData { export interface SessionData {
radios?: { frequency: number; modulation: number; pan: number }[]; radios?: { frequency: number; modulation: number; pan: number }[];
fileSources?: { filename: string; volume: number }[]; fileSources?: { filename: string; volume: number }[];
unitSinks?: { ID: number }[]; unitSinks?: { ID: number }[];
connections?: any[]; connections?: any[];
coalitionAreas?: ( coalitionAreas?: (
| { type: "circle"; label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition } | { type: "circle"; label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition }
| { type: "polygon"; label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition } | { type: "polygon"; label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition }
)[]; )[];
hotgroups?: { [key: string]: number[] }; hotgroups?: { [key: string]: number[] };
starredSpawns?: { [key: number]: SpawnRequestTable }; starredSpawns?: { [key: number]: SpawnRequestTable };
drawings?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } }; 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 } }; navpoints?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } };
mapSource?: { id: string }; mapSource?: { id: string };
customLoadouts?: { [key: string]: LoadoutBlueprint[] };
} }
export interface ProfileOptions { export interface ProfileOptions {
mapOptions?: MapOptions; mapOptions?: MapOptions;
shortcuts?: { [key: string]: ShortcutOptions }; shortcuts?: { [key: string]: ShortcutOptions };
audioOptions?: AudioOptions; audioOptions?: AudioOptions;
} }
export interface ContextMenuOption { export interface ContextMenuOption {
tooltip: string; tooltip: string;
src: string; src: string;
callback: CallableFunction; callback: CallableFunction;
} }
export interface AirbasesData { export interface AirbasesData {
airbases: { [key: string]: any }; airbases: { [key: string]: any };
sessionHash: string; sessionHash: string;
time: number; time: number;
} }
export interface BullseyesData { export interface BullseyesData {
bullseyes: { bullseyes: {
[key: string]: { latitude: number; longitude: number; coalition: string }; [key: string]: { latitude: number; longitude: number; coalition: string };
}; };
sessionHash: string; sessionHash: string;
time: number; time: number;
} }
export interface SpotsData { export interface SpotsData {
spots: { spots: {
[key: string]: { active: boolean; type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number }; [key: string]: { active: boolean; type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number };
}; };
sessionHash: string; sessionHash: string;
time: number; time: number;
} }
export interface MissionData { export interface MissionData {
mission: { mission: {
theatre: string; theatre: string;
dateAndTime: DateAndTime; dateAndTime: DateAndTime;
commandModeOptions: CommandModeOptions; commandModeOptions: CommandModeOptions;
coalitions: { red: string[]; blue: string[] }; coalitions: { red: string[]; blue: string[] };
}; };
time: number; time: number;
sessionHash: string; sessionHash: string;
} }
export interface CommandModeOptions { export interface CommandModeOptions {
commandMode: string; commandMode: string;
restrictSpawns: boolean; restrictSpawns: boolean;
restrictToCoalition: boolean; restrictToCoalition: boolean;
setupTime: number; setupTime: number;
spawnPoints: { spawnPoints: {
red: number; red: number;
blue: number; blue: number;
}; };
eras: string[]; eras: string[];
} }
export interface DateAndTime { export interface DateAndTime {
date: { Year: number; Month: number; Day: number }; date: { Year: number; Month: number; Day: number };
time: { h: number; m: number; s: number }; time: { h: number; m: number; s: number };
elapsedTime: number; elapsedTime: number;
startTime: number; startTime: number;
} }
export interface LogData { export interface LogData {
logs: { [key: string]: string }; logs: { [key: string]: string };
sessionHash: string; sessionHash: string;
time: number; time: number;
} }
export interface ServerRequestOptions { export interface ServerRequestOptions {
time?: number; time?: number;
commandHash?: string; commandHash?: string;
} }
export interface SpawnRequestTable { export interface SpawnRequestTable {
category: string; category: string;
coalition: string; coalition: string;
unit: UnitSpawnTable; unit: UnitSpawnTable;
amount: number; amount: number;
quickAccessName?: string; quickAccessName?: string;
} }
export interface EffectRequestTable { export interface EffectRequestTable {
type: string; type: string;
explosionType?: string; explosionType?: string;
smokeColor?: string; smokeColor?: string;
} }
export interface UnitSpawnTable { export interface UnitSpawnTable {
unitType: string; unitType: string;
location: LatLng; location: LatLng;
skill: string; skill: string;
liveryID: string; liveryID: string;
altitude?: number; altitude?: number;
loadout?: string; loadout?: string;
heading?: number; heading?: number;
payload?: string;
} }
export interface ObjectIconOptions { export interface ObjectIconOptions {
showState: boolean; showState: boolean;
showVvi: boolean; showVvi: boolean;
showHealth: boolean; showHealth: boolean;
showHotgroup: boolean; showHotgroup: boolean;
showUnitIcon: boolean; showUnitIcon: boolean;
showShortLabel: boolean; showShortLabel: boolean;
showFuel: boolean; showFuel: boolean;
showAmmo: boolean; showAmmo: boolean;
showSummary: boolean; showSummary: boolean;
showCallsign: boolean; showCallsign: boolean;
rotateToHeading: boolean; rotateToHeading: boolean;
showCluster: boolean; showCluster: boolean;
showAlarmState: boolean; showAlarmState: boolean;
} }
export interface GeneralSettings { export interface GeneralSettings {
prohibitJettison: boolean; prohibitJettison: boolean;
prohibitAA: boolean; prohibitAA: boolean;
prohibitAG: boolean; prohibitAG: boolean;
prohibitAfterburner: boolean; prohibitAfterburner: boolean;
prohibitAirWpn: boolean; prohibitAirWpn: boolean;
} }
export interface TACAN { export interface TACAN {
isOn: boolean; isOn: boolean;
channel: number; channel: number;
XY: string; XY: string;
callsign: string; callsign: string;
} }
export interface Radio { export interface Radio {
frequency: number; frequency: number;
callsign: number; callsign: number;
callsignNumber: number; callsignNumber: number;
} }
export interface Ammo { export interface Ammo {
quantity: number; quantity: number;
name: string; name: string;
guidance: number; guidance: number;
category: number; category: number;
missileCategory: number; missileCategory: number;
} }
export interface Contact { export interface Contact {
ID: number; ID: number;
detectionMethod: number; detectionMethod: number;
} }
export interface Offset { export interface Offset {
x: number; x: number;
y: number; y: number;
z: number; z: number;
}
export interface DrawingArgument {
argument: number;
value: number;
} }
export interface UnitData { export interface UnitData {
category: string; category: string;
markerCategory: string; markerCategory: string;
ID: number; ID: number;
alive: boolean; alive: boolean;
alarmState: AlarmState; alarmState: AlarmState;
human: boolean; human: boolean;
controlled: boolean; controlled: boolean;
coalition: string; coalition: string;
country: number; country: number;
name: string; name: string;
unitName: string; unitName: string;
callsign: string; callsign: string;
unitID: number; unitID: number;
groupID: number; groupID: number;
groupName: string; groupName: string;
state: string; state: string;
task: string; task: string;
hasTask: boolean; hasTask: boolean;
position: LatLng; position: LatLng;
speed: number; speed: number;
horizontalVelocity: number; horizontalVelocity: number;
verticalVelocity: number; verticalVelocity: number;
heading: number; heading: number;
track: number; track: number;
isActiveTanker: boolean; isActiveTanker: boolean;
isActiveAWACS: boolean; isActiveAWACS: boolean;
onOff: boolean; onOff: boolean;
followRoads: boolean; followRoads: boolean;
fuel: number; fuel: number;
desiredSpeed: number; desiredSpeed: number;
desiredSpeedType: string; desiredSpeedType: string;
desiredAltitude: number; desiredAltitude: number;
desiredAltitudeType: string; desiredAltitudeType: string;
leaderID: number; leaderID: number;
formationOffset: Offset; formationOffset: Offset;
targetID: number; targetID: number;
targetPosition: LatLng; targetPosition: LatLng;
ROE: string; ROE: string;
reactionToThreat: string; reactionToThreat: string;
emissionsCountermeasures: string; emissionsCountermeasures: string;
TACAN: TACAN; TACAN: TACAN;
radio: Radio; radio: Radio;
generalSettings: GeneralSettings; generalSettings: GeneralSettings;
ammo: Ammo[]; ammo: Ammo[];
contacts: Contact[]; contacts: Contact[];
activePath: LatLng[]; activePath: LatLng[];
isLeader: boolean; isLeader: boolean;
operateAs: string; operateAs: string;
shotsScatter: number; shotsScatter: number;
shotsIntensity: number; shotsIntensity: number;
health: number; health: number;
racetrackLength: number; racetrackLength: number;
racetrackAnchor: LatLng; racetrackAnchor: LatLng;
racetrackBearing: number; racetrackBearing: number;
timeToNextTasking: number; timeToNextTasking: number;
barrelHeight: number; barrelHeight: number;
muzzleVelocity: number; muzzleVelocity: number;
aimTime: number; aimTime: number;
shotsToFire: number; shotsToFire: number;
shotsBaseInterval: number; shotsBaseInterval: number;
shotsBaseScatter: number; shotsBaseScatter: number;
engagementRange: number; engagementRange: number;
targetingRange: number; targetingRange: number;
aimMethodRange: number; aimMethodRange: number;
acquisitionRange: number; acquisitionRange: number;
airborne: boolean; airborne: boolean;
cargoWeight: number;
drawingArguments: DrawingArgument[];
customString: string;
customInteger: number;
} }
export interface LoadoutItemBlueprint { export interface LoadoutItemBlueprint {
name: string; name: string;
quantity: number; quantity: number;
effectiveAgainst?: string;
} }
export interface LoadoutBlueprint { export interface LoadoutBlueprint {
fuel: number; items: LoadoutItemBlueprint[];
items: LoadoutItemBlueprint[]; roles: string[];
roles: string[]; code: string;
code: string; name: string;
name: string; enabled: boolean;
enabled: boolean; isCustom?: boolean;
persistent?: boolean;
payload?: string;
} }
export interface UnitBlueprint { export interface UnitBlueprint {
name: string; name: string;
category: string; category: string;
enabled: boolean; enabled: boolean;
coalition: string; coalition: string;
era: string; era: string;
label: string; label: string;
shortLabel: string; shortLabel: string;
roles?: string[]; roles?: string[];
type?: string; type?: string;
loadouts?: LoadoutBlueprint[]; loadouts?: LoadoutBlueprint[];
acceptedPayloads?: { [key: string]: { clsids: string[]; names: string[] } }; acceptedPayloads?: { [key: string]: { clsid: string; name: string; weight: number }[] };
filename?: string; filename?: string;
liveries?: { [key: string]: { name: string; countries: string[] } }; liveries?: { [key: string]: { name: string; countries: string[] } };
cost?: number; cost?: number;
barrelHeight?: number; barrelHeight?: number;
muzzleVelocity?: number; muzzleVelocity?: number;
aimTime?: number; aimTime?: number;
shotsToFire?: number; shotsToFire?: number;
shotsBaseInterval?: number; shotsBaseInterval?: number;
shotsBaseScatter?: number; shotsBaseScatter?: number;
description?: string; description?: string;
abilities?: string; abilities?: string;
tags?: string; tags?: string;
acquisitionRange?: number; acquisitionRange?: number;
engagementRange?: number; engagementRange?: number;
targetingRange?: number; targetingRange?: number;
aimMethodRange?: number; aimMethodRange?: number;
alertnessTimeConstant?: number; alertnessTimeConstant?: number;
canTargetPoint?: boolean; canTargetPoint?: boolean;
canRearm?: boolean; canRearm?: boolean;
canAAA?: boolean; canAAA?: boolean;
indirectFire?: boolean; indirectFire?: boolean;
markerFile?: string; markerFile?: string;
unitWhenGrouped?: string; unitWhenGrouped?: string;
mainRole?: string; mainRole?: string;
length?: number; length?: number;
carrierFilename?: string; carrierFilename?: string;
} }
export interface AirbaseOptions { export interface AirbaseOptions {
name: string; name: string;
position: L.LatLng; position: L.LatLng;
} }
export interface AirbaseChartData { export interface AirbaseChartData {
elevation: string; elevation: string;
ICAO: string; ICAO: string;
TACAN: string; TACAN: string;
runways: AirbaseChartRunwayData[]; runways: AirbaseChartRunwayData[];
} }
export interface AirbaseChartRunwayHeadingData { export interface AirbaseChartRunwayHeadingData {
[index: string]: { [index: string]: {
magHeading: string; magHeading: string;
ILS: string; ILS: string;
}; };
} }
export interface AirbaseChartRunwayData { export interface AirbaseChartRunwayData {
headings: AirbaseChartRunwayHeadingData[]; headings: AirbaseChartRunwayHeadingData[];
length: string; length: string;
} }
export interface ShortcutOptions { export interface ShortcutOptions {
label: string; label: string;
keyUpCallback: (e: KeyboardEvent) => void; keyUpCallback: (e: KeyboardEvent) => void;
keyDownCallback?: (e: KeyboardEvent) => void; keyDownCallback?: (e: KeyboardEvent) => void;
code: string; code: string;
altKey?: boolean; altKey?: boolean;
ctrlKey?: boolean; ctrlKey?: boolean;
shiftKey?: boolean; shiftKey?: boolean;
} }
export interface ServerStatus { export interface ServerStatus {
frameRate: number; frameRate: number;
load: number; load: number;
elapsedTime: number; elapsedTime: number;
missionTime: DateAndTime["time"]; missionTime: DateAndTime["time"];
connected: boolean; connected: boolean;
paused: boolean; paused: boolean;
} }
export type DrawingPoint = { export type DrawingPoint = {
x: number; x: number;
y: number; y: number;
}; };
export type PolygonPoints = DrawingPoint[] | DrawingPoint; export type PolygonPoints = DrawingPoint[] | DrawingPoint;
@@ -396,36 +408,36 @@ export type PolygonPoints = DrawingPoint[] | DrawingPoint;
export type DrawingPrimitiveType = "TextBox" | "Polygon" | "Line" | "Icon"; export type DrawingPrimitiveType = "TextBox" | "Polygon" | "Line" | "Icon";
export interface Drawing { export interface Drawing {
name: string; name: string;
visible: boolean; visible: boolean;
mapX: number; mapX: number;
mapY: number; mapY: number;
layerName: string; layerName: string;
layer: string; layer: string;
primitiveType: DrawingPrimitiveType; primitiveType: DrawingPrimitiveType;
colorString: string; colorString: string;
fillColorString?: string; fillColorString?: string;
borderThickness?: number; borderThickness?: number;
fontSize?: number; fontSize?: number;
font?: string; font?: string;
text?: string; text?: string;
angle?: number; angle?: number;
radius?: number; radius?: number;
points?: PolygonPoints; points?: PolygonPoints;
style?: string; style?: string;
polygonMode?: string; polygonMode?: string;
thickness?: number; thickness?: number;
width?: number; width?: number;
height?: number; height?: number;
closed?: boolean; closed?: boolean;
lineMode?: string; lineMode?: string;
hiddenOnPlanner?: boolean; hiddenOnPlanner?: boolean;
file?: string; file?: string;
scale?: number; scale?: number;
} }
export enum AlarmState { export enum AlarmState {
RED = 'red', RED = "red",
GREEN = 'green', GREEN = "green",
AUTO = 'auto' AUTO = "auto",
} }

View File

@@ -0,0 +1,18 @@
import * as L from "leaflet";
export class LatLng extends L.LatLng {
threshold: number;
constructor(lat: number, lng: number, alt: number, threshold: number) {
super(lat, lng, alt);
this.threshold = threshold;
}
setThreshold(threshold: number) {
this.threshold = threshold;
}
getThreshold() {
return this.threshold;
}
}

View File

@@ -443,7 +443,7 @@ export class Map extends L.Map {
ctrlKey: false, 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; const contextAction = ContextActions[contextActionName] as ContextAction;
if (contextAction.getOptions().code) { if (contextAction.getOptions().code) {
getApp() getApp()
@@ -560,10 +560,15 @@ export class Map extends L.Map {
} }
}) })
.then((res: any) => { .then((res: any) => {
if ("alt-" + theatre.toLowerCase() in res) { // Convert the result keys to lower case to avoid case sensitivity issues
let template = `${mirror}/alt-${theatre.toLowerCase()}/{z}/{x}/{y}.png`; 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( layers.push(
...res["alt-" + theatre.toLowerCase()].map((layerConfig: any) => { ...res[key].map((layerConfig: any) => {
return new L.TileLayer(template, { return new L.TileLayer(template, {
...layerConfig, ...layerConfig,
crossOrigin: "", crossOrigin: "",
@@ -626,13 +631,13 @@ export class Map extends L.Map {
return this.#spawnHeading; return this.#spawnHeading;
} }
addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable, quickAccessName: string) { addStarredSpawnRequestTable(key: string, spawnRequestTable: SpawnRequestTable, quickAccessName: string) {
this.#starredSpawnRequestTables[key] = spawnRequestTable; this.#starredSpawnRequestTables[key] = spawnRequestTable;
this.#starredSpawnRequestTables[key].quickAccessName = quickAccessName; this.#starredSpawnRequestTables[key].quickAccessName = quickAccessName;
StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables); StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables);
} }
removeStarredSpawnRequestTable(key) { removeStarredSpawnRequestTable(key: string) {
if (key in this.#starredSpawnRequestTables) delete this.#starredSpawnRequestTables[key]; if (key in this.#starredSpawnRequestTables) delete this.#starredSpawnRequestTables[key];
StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables); StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables);
} }
@@ -673,7 +678,7 @@ export class Map extends L.Map {
} }
setHiddenType(key: string, value: boolean) { setHiddenType(key: string, value: boolean) {
this.#hiddenTypes[key] = value; this.#hiddenTypes[key as keyof MapHiddenTypes] = value;
HiddenTypesChangedEvent.dispatch(this.#hiddenTypes); HiddenTypesChangedEvent.dispatch(this.#hiddenTypes);
} }
@@ -783,13 +788,13 @@ export class Map extends L.Map {
return smokeMarker; return smokeMarker;
} }
setOption(key, value) { setOption<K extends keyof MapOptions>(key: K, value: MapOptions[K]) {
this.#options[key] = value; this.#options[key] = value;
MapOptionsChangedEvent.dispatch(this.#options, key); MapOptionsChangedEvent.dispatch(this.#options, key as keyof MapOptions);
} }
setOptions(options) { setOptions(options: Partial<MapOptions>) {
this.#options = { ...options }; this.#options = { ...this.#options, ...options } as MapOptions;
MapOptionsChangedEvent.dispatch(this.#options); MapOptionsChangedEvent.dispatch(this.#options);
} }
@@ -1066,7 +1071,7 @@ export class Map extends L.Map {
false, false,
undefined, undefined,
undefined, undefined,
(hash) => { (hash: string) => {
this.addTemporaryMarker( this.addTemporaryMarker(
e.latlng, e.latlng,
this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.unit.unitType ?? "unknown",
@@ -1234,7 +1239,7 @@ export class Map extends L.Map {
this.#lastMouseCoordinates = e.latlng; this.#lastMouseCoordinates = e.latlng;
MouseMovedEvent.dispatch(e.latlng); MouseMovedEvent.dispatch(e.latlng);
getGroundElevation(e.latlng, (elevation) => { getGroundElevation(e.latlng, (elevation: number) => {
MouseMovedEvent.dispatch(e.latlng, elevation); MouseMovedEvent.dispatch(e.latlng, elevation);
}); });
@@ -1361,8 +1366,8 @@ export class Map extends L.Map {
.filter((unit) => !unit.getHuman()); .filter((unit) => !unit.getHuman());
Object.keys(this.#destinationPreviewMarkers).forEach((ID) => { Object.keys(this.#destinationPreviewMarkers).forEach((ID) => {
this.#destinationPreviewMarkers[ID].removeFrom(this); this.#destinationPreviewMarkers[parseInt(ID)].removeFrom(this);
delete this.#destinationPreviewMarkers[ID]; delete this.#destinationPreviewMarkers[parseInt(ID)];
}); });
if (this.#keepRelativePositions) { if (this.#keepRelativePositions) {
@@ -1380,7 +1385,7 @@ export class Map extends L.Map {
#moveDestinationPreviewMarkers() { #moveDestinationPreviewMarkers() {
if (this.#keepRelativePositions) { if (this.#keepRelativePositions) {
Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#destinationRotationCenter, this.#destinationRotation)).forEach(([ID, latlng]) => { Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#destinationRotationCenter, this.#destinationRotation)).forEach(([ID, latlng]) => {
this.#destinationPreviewMarkers[ID]?.setLatLng(latlng); this.#destinationPreviewMarkers[parseInt(ID)]?.setLatLng(latlng);
}); });
} else { } else {
Object.values(this.#destinationPreviewMarkers).forEach((marker) => { Object.values(this.#destinationPreviewMarkers).forEach((marker) => {

View File

@@ -310,11 +310,13 @@ export class OlympusApp {
} }
setState(state: OlympusState, subState: OlympusSubState = NO_SUBSTATE) { setState(state: OlympusState, subState: OlympusSubState = NO_SUBSTATE) {
const previousState = this.#state;
const previousSubState = this.#subState;
this.#state = state; this.#state = state;
this.#subState = subState; this.#subState = subState;
console.log(`App state set to ${state}, substate ${subState}`); console.log(`App state set to ${state}, substate ${subState}`);
AppStateChangedEvent.dispatch(state, subState); AppStateChangedEvent.dispatch(state, subState, previousState, previousSubState);
} }
getState() { getState() {

View File

@@ -1,5 +1,5 @@
import { LatLng } from "leaflet"; 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 { export class DataExtractor {
#seekPosition = 0; #seekPosition = 0;
@@ -58,7 +58,9 @@ export class DataExtractor {
} }
extractLatLng() { 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) { extractFromBitmask(bitmask: number, position: number) {
@@ -104,6 +106,14 @@ export class DataExtractor {
return value; return value;
} }
extractDrawingArgument() {
const value: DrawingArgument = {
argument: this.extractUInt32(),
value: this.extractFloat64(),
};
return value;
}
extractGeneralSettings() { extractGeneralSettings() {
const value: GeneralSettings = { const value: GeneralSettings = {
prohibitJettison: this.extractBool(), prohibitJettison: this.extractBool(),
@@ -159,4 +169,13 @@ export class DataExtractor {
}; };
return value; return value;
} }
extractDrawingArguments() {
const value: DrawingArgument[] = [];
const size = this.extractUInt16();
for (let idx = 0; idx < size; idx++) {
value.push(this.extractDrawingArgument());
}
return value;
}
} }

View File

@@ -8,6 +8,7 @@ import {
AudioSinksChangedEvent, AudioSinksChangedEvent,
AudioSourcesChangedEvent, AudioSourcesChangedEvent,
CoalitionAreasChangedEvent, CoalitionAreasChangedEvent,
CustomLoadoutsUpdatedEvent,
DrawingsUpdatedEvent, DrawingsUpdatedEvent,
HotgroupsChangedEvent, HotgroupsChangedEvent,
MapSourceChangedEvent, MapSourceChangedEvent,
@@ -16,7 +17,7 @@ import {
SessionDataSavedEvent, SessionDataSavedEvent,
StarredSpawnsChangedEvent, StarredSpawnsChangedEvent,
} from "./events"; } from "./events";
import { SessionData } from "./interfaces"; import { LoadoutBlueprint, SessionData } from "./interfaces";
import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle"; import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle";
import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon";
import { getApp } from "./olympusapp"; import { getApp } from "./olympusapp";
@@ -124,7 +125,7 @@ export class SessionDataManager {
HotgroupsChangedEvent.on((hotgroups) => { HotgroupsChangedEvent.on((hotgroups) => {
this.#sessionData.hotgroups = {}; this.#sessionData.hotgroups = {};
Object.keys(hotgroups).forEach((hotgroup) => { 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(); this.#saveSessionData();
}); });
@@ -146,6 +147,16 @@ export class SessionDataManager {
this.#sessionData.mapSource = { id: source }; this.#sessionData.mapSource = { id: source };
this.#saveSessionData(); 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); }, 200);
}); });
} }

View File

@@ -156,8 +156,8 @@ export function OlDropdown(props: {
data-open={open} data-open={open}
className={` className={`
absolute z-40 divide-y divide-gray-100 overflow-y-scroll absolute z-40 divide-y divide-gray-100 overflow-y-scroll
no-scrollbar rounded-lg bg-white p-2 shadow no-scrollbar rounded-lg border border-2 border-gray-600 bg-gray-700
dark:bg-gray-700 p-2 shadow
data-[open='false']:hidden data-[open='false']:hidden
`} `}
> >
@@ -187,7 +187,7 @@ export function OlDropdown(props: {
} }
/* Conveniency Component for dropdown elements */ /* 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 ( return (
<button <button
onClick={props.onClick ?? (() => {})} onClick={props.onClick ?? (() => {})}
@@ -195,8 +195,11 @@ export function OlDropdownItem(props: { onClick?: () => void; className?: string
${props.className ?? ""} ${props.className ?? ""}
flex w-full cursor-pointer select-none flex-row content-center flex w-full cursor-pointer select-none flex-row content-center
rounded-md px-4 py-2 rounded-md px-4 py-2
dark:hover:bg-gray-600 dark:hover:text-white hover:bg-gray-600 hover:text-white
hover:bg-gray-100 ${props.disabled ? `
cursor-default opacity-50
hover:bg-transparent hover:text-gray-200
` : ``}
`} `}
style={{ style={{
border: props.borderColor ? `2px solid ${props.borderColor}` : "2px solid transparent", border: props.borderColor ? `2px solid ${props.borderColor}` : "2px solid transparent",

View File

@@ -8,6 +8,7 @@ export function OlNumberInput(props: {
max: number; max: number;
minLength?: number; minLength?: number;
className?: string; className?: string;
internalClassName?: string;
tooltip?: string | (() => JSX.Element | JSX.Element[]); tooltip?: string | (() => JSX.Element | JSX.Element[]);
tooltipPosition?: string; tooltipPosition?: string;
tooltipRelativeToParent?: boolean; tooltipRelativeToParent?: boolean;
@@ -34,7 +35,10 @@ export function OlNumberInput(props: {
`} `}
> >
<div <div
className="relative flex max-w-[8rem] items-center" className={`
relative flex max-w-[8rem] items-center
${props.internalClassName ?? ""}
`}
ref={buttonRef} ref={buttonRef}
onMouseEnter={() => { onMouseEnter={() => {
setHoverTimeout( setHoverTimeout(

View File

@@ -8,8 +8,9 @@ export function Modal(props: {
open: boolean; open: boolean;
children?: JSX.Element | JSX.Element[]; children?: JSX.Element | JSX.Element[];
className?: string; className?: string;
size?: "sm" | "md" | "lg" | "full"; size?: "sm" | "md" | "lg" | "full" | "tall";
disableClose?: boolean; disableClose?: boolean;
onClose?: () => void;
}) { }) {
const [splash, setSplash] = useState(Math.ceil(Math.random() * 7)); 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" : ""} ${props.size === "full" ? "h-full w-full" : ""}
`} `}
> >
@@ -90,7 +99,7 @@ export function Modal(props: {
> >
<FaXmark <FaXmark
onClick={() => { onClick={() => {
getApp().setState(OlympusState.IDLE); props.onClose ? props.onClose() : getApp().setState(OlympusState.IDLE);
}} }}
/>{" "} />{" "}
</div> </div>

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -142,6 +142,31 @@ export function MainMenu(props: { open: boolean; onClose: () => void; children?:
/> />
</div> </div>
</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> </div>
</Menu> </Menu>
); );

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ import { UnitControlMenu } from "./panels/unitcontrolmenu";
import { MainMenu } from "./panels/mainmenu"; import { MainMenu } from "./panels/mainmenu";
import { SideBar } from "./panels/sidebar"; import { SideBar } from "./panels/sidebar";
import { OptionsMenu } from "./panels/optionsmenu"; 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 { getApp, setupApp } from "../olympusapp";
import { LoginModal } from "./modals/loginmodal"; import { LoginModal } from "./modals/loginmodal";
@@ -31,6 +31,8 @@ import { ImportExportModal } from "./modals/importexportmodal";
import { WarningModal } from "./modals/warningmodal"; import { WarningModal } from "./modals/warningmodal";
import { TrainingModal } from "./modals/trainingmodal"; import { TrainingModal } from "./modals/trainingmodal";
import { AdminModal } from "./modals/adminmodal"; import { AdminModal } from "./modals/adminmodal";
import { ImageOverlayModal } from "./modals/imageoverlaymodal";
import { LoadoutWizardModal } from "./modals/loadoutwizardmodal";
export function UI() { export function UI() {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
@@ -74,6 +76,8 @@ export function UI() {
<WarningModal open={appState === OlympusState.WARNING} /> <WarningModal open={appState === OlympusState.WARNING} />
<TrainingModal open={appState === OlympusState.TRAINING} /> <TrainingModal open={appState === OlympusState.TRAINING} />
<AdminModal open={appState === OlympusState.ADMIN} /> <AdminModal open={appState === OlympusState.ADMIN} />
<ImageOverlayModal open={appState === OlympusState.IMPORT_IMAGE_OVERLAY} />
<LoadoutWizardModal open={appState === OlympusState.SPAWN && appSubState === SpawnSubState.LOADOUT_WIZARD} />
</> </>
)} )}

View File

@@ -1,12 +1,26 @@
import { getApp } from "../../olympusapp"; import { getApp } from "../../olympusapp";
import { GAME_MASTER } from "../../constants/constants"; import { GAME_MASTER } from "../../constants/constants";
import { UnitBlueprint } from "../../interfaces"; import { UnitBlueprint } from "../../interfaces";
import { UnitDatabaseLoadedEvent } from "../../events"; import { SessionDataLoadedEvent, UnitDatabaseLoadedEvent } from "../../events";
export class UnitDatabase { export class UnitDatabase {
blueprints: { [key: string]: UnitBlueprint } = {}; 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) { load(url: string, category?: string) {
if (url !== "") { if (url !== "") {
@@ -204,7 +218,7 @@ export class UnitDatabase {
getLoadoutNamesByRole(name: string, role: string) { getLoadoutNamesByRole(name: string, role: string) {
var filteredBlueprints = this.getBlueprints(); var filteredBlueprints = this.getBlueprints();
var loadoutsByRole: string[] = []; var loadoutsByRole: string[] = [];
var loadouts = filteredBlueprints[name].loadouts; var loadouts = filteredBlueprints[name as any].loadouts;
if (loadouts) { if (loadouts) {
for (let loadout of loadouts) { for (let loadout of loadouts) {
if (loadout.roles.includes(role) || loadout.roles.includes("")) { if (loadout.roles.includes(role) || loadout.roles.includes("")) {

View File

@@ -1,4 +1,4 @@
import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point, LeafletMouseEvent, DomEvent, DomUtil, Circle } from "leaflet"; import { LatLng, Polyline, DivIcon, CircleMarker, Map, Point, DomEvent } from "leaflet";
import { getApp } from "../olympusapp"; import { getApp } from "../olympusapp";
import { import {
enumToCoalition, enumToCoalition,
@@ -54,7 +54,7 @@ import {
} from "../constants/constants"; } from "../constants/constants";
import { DataExtractor } from "../server/dataextractor"; import { DataExtractor } from "../server/dataextractor";
import { Weapon } from "../weapon/weapon"; 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 { RangeCircle } from "../map/rangecircle";
import { Group } from "./group"; import { Group } from "./group";
import { ContextActionSet } from "./contextactionset"; import { ContextActionSet } from "./contextactionset";
@@ -159,6 +159,10 @@ export abstract class Unit extends CustomMarker {
#racetrackAnchor: LatLng = new LatLng(0, 0); #racetrackAnchor: LatLng = new LatLng(0, 0);
#racetrackBearing: number = 0; #racetrackBearing: number = 0;
#airborne: boolean = false; #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 */ /* Other members used to draw the unit, mostly ancillary stuff like targets, ranges and so on */
#blueprint: UnitBlueprint | null = null; #blueprint: UnitBlueprint | null = null;
@@ -406,6 +410,18 @@ export abstract class Unit extends CustomMarker {
getAirborne() { getAirborne() {
return this.#airborne; return this.#airborne;
} }
getCargoWeight() {
return this.#cargoWeight;
}
getDrawingArguments() {
return this.#drawingArguments;
}
getCustomString() {
return this.#customString;
}
getCustomInteger() {
return this.#customInteger;
}
static getConstructor(type: string) { static getConstructor(type: string) {
if (type === "GroundUnit") return GroundUnit; if (type === "GroundUnit") return GroundUnit;
@@ -797,6 +813,18 @@ export abstract class Unit extends CustomMarker {
case DataIndexes.airborne: case DataIndexes.airborne:
this.#airborne = dataExtractor.extractBool(); this.#airborne = dataExtractor.extractBool();
break; 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: default:
break; break;
} }
@@ -920,6 +948,10 @@ export abstract class Unit extends CustomMarker {
aimMethodRange: this.#aimMethodRange, aimMethodRange: this.#aimMethodRange,
acquisitionRange: this.#acquisitionRange, acquisitionRange: this.#acquisitionRange,
airborne: this.#airborne, airborne: this.#airborne,
cargoWeight: this.#cargoWeight,
drawingArguments: this.#drawingArguments,
customString: this.#customString,
customInteger: this.#customInteger
}; };
} }

View File

@@ -23,6 +23,7 @@ Olympus.unitIndex = 0 -- Counter used to spread the computational load of data
Olympus.unitStep = 50 -- Max number of units that get updated each cycle Olympus.unitStep = 50 -- Max number of units that get updated each cycle
Olympus.units = {} -- Table holding references to all the currently existing units 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.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.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 Olympus.weaponStep = 50 -- Max number of weapons that get updated each cycle
@@ -1087,10 +1088,38 @@ function Olympus.setOnOff(groupName, onOff)
end end
end end
-- Get the unit description
function getUnitDescription(unit) function getUnitDescription(unit)
return unit:getDescr() return unit:getDescr()
end 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 -- This function gets the navpoints from the DCS mission
function Olympus.getNavPoints() function Olympus.getNavPoints()
local function extract_tag(str) local function extract_tag(str)
@@ -1294,6 +1323,20 @@ function Olympus.setUnitsData(arg, time)
end end
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() local group = unit:getGroup()
if group ~= nil then if group ~= nil then
local controller = group:getController() local controller = group:getController()

View File

@@ -35,7 +35,7 @@ mist = {}
-- don't change these -- don't change these
mist.majorVersion = 4 mist.majorVersion = 4
mist.minorVersion = 5 mist.minorVersion = 5
mist.build = 122 mist.build = 125
-- forward declaration of log shorthand -- forward declaration of log shorthand
local log local log
@@ -695,7 +695,6 @@ do -- the main scope
["FARP"] = "farps", ["FARP"] = "farps",
["Fueltank"] = "fueltank_cargo", ["Fueltank"] = "fueltank_cargo",
["Gate"] = "gate", ["Gate"] = "gate",
["FARP Fuel Depot"] = "gsm rus",
["Armed house"] = "home1_a", ["Armed house"] = "home1_a",
["FARP Command Post"] = "kp-ug", ["FARP Command Post"] = "kp-ug",
["Watch Tower Armed"] = "ohr-vyshka", ["Watch Tower Armed"] = "ohr-vyshka",
@@ -704,7 +703,6 @@ do -- the main scope
["Pipes big"] = "pipes_big_cargo", ["Pipes big"] = "pipes_big_cargo",
["Oil platform"] = "plavbaza", ["Oil platform"] = "plavbaza",
["Tetrapod"] = "tetrapod_cargo", ["Tetrapod"] = "tetrapod_cargo",
["Fuel tank"] = "toplivo",
["Trunks long"] = "trunks_long_cargo", ["Trunks long"] = "trunks_long_cargo",
["Trunks small"] = "trunks_small_cargo", ["Trunks small"] = "trunks_small_cargo",
["Passenger liner"] = "yastrebow", ["Passenger liner"] = "yastrebow",
@@ -1152,6 +1150,7 @@ do -- the main scope
end end
end end
end end
--dbLog:warn(newTable)
--mist.debug.writeData(mist.utils.serialize,{'msg', newTable}, timer.getAbsTime() ..'Group.lua') --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 newTable.timeAdded = timer.getAbsTime() -- only on the dynGroupsAdded table. For other reference, see start time
--mist.debug.dumpDBs() --mist.debug.dumpDBs()
@@ -1493,7 +1492,7 @@ do -- the main scope
task.t = timer.getTime() + task.rep --schedule next run task.t = timer.getTime() + task.rep --schedule next run
local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars))) local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))
if not err then if not err then
log:error('Error in scheduled function: $1' .. errmsg) log:error('Error in scheduled function: $1', errmsg)
end end
--scheduledTasks[i].f(unpack(scheduledTasks[i].vars, 1, table.maxn(scheduledTasks[i].vars))) -- do the task --scheduledTasks[i].f(unpack(scheduledTasks[i].vars, 1, table.maxn(scheduledTasks[i].vars))) -- do the task
i = i + 1 i = i + 1
@@ -1519,7 +1518,7 @@ do -- the main scope
id = tostring(original_id) .. ' #' .. tostring(id_ind) id = tostring(original_id) .. ' #' .. tostring(id_ind)
id_ind = id_ind + 1 id_ind = id_ind + 1
end end
local valid
if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then
--log:info('object found in alive_units') --log:info('object found in alive_units')
val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_]) 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) --trigger.action.outText('remove via death: ' .. Unit.getName(val.object),20)
mist.DBs.activeHumans[Unit.getName(val.object)] = nil mist.DBs.activeHumans[Unit.getName(val.object)] = nil
end]] 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 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') --log:info('object found in old_alive_units')
val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_]) val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])
@@ -1540,32 +1540,37 @@ do -- the main scope
val.objectPos = pos.p val.objectPos = pos.p
end end
val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category
valid = true
else --attempt to determine if static object... else --attempt to determine if static object...
--log:info('object not found in alive units or old alive units') --log:info('object not found in alive units or old alive units')
local pos = Object.getPosition(val.object) if Object.isExist(val.object) then
if pos then local pos = Object.getPosition(val.object)
local static_found = false if pos then
for ind, static in pairs(mist.DBs.unitsByCat.static) do local static_found = false
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... for ind, static in pairs(mist.DBs.unitsByCat.static) do
--log:info('correlated dead static object to position') 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...
val.objectData = static --log:info('correlated dead static object to position')
val.objectPos = pos.p val.objectData = static
val.objectType = 'static' val.objectPos = pos.p
static_found = true val.objectType = 'static'
break static_found = true
break
end
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 end
if not static_found then valid = true
val.objectPos = pos.p
val.objectType = 'building'
val.typeName = Object.getTypeName(val.object)
end
else
val.objectType = 'unknown'
end end
end end
mist.DBs.deadObjects[id] = val if valid then
mist.DBs.deadObjects[id] = val
end
end end
end end
end end
@@ -2019,7 +2024,7 @@ do -- the main scope
end end
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) --log:warn(newGroup)
-- sanitize table -- sanitize table
newGroup.groupName = nil newGroup.groupName = nil
@@ -3560,7 +3565,7 @@ function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_ty
end end
function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius) 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 radius = radius or math.huge
local unit_info1 = {} local unit_info1 = {}
local unit_info2 = {} 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. -- get the positions all in one step, saves execution time.
for unitset1_ind = 1, #unitset1 do for unitset1_ind = 1, #unitset1 do
local unit1 = Unit.getByName(unitset1[unitset1_ind]) local unit1 = Unit.getByName(unitset1[unitset1_ind])
local lCat = Object.getCategory(unit1) if unit1 then
if unit1 and ((lCat == 1 and unit1:isActive()) or lCat ~= 1) and unit:isExist() == true then local lCat = Object.getCategory(unit1)
unit_info1[#unit_info1 + 1] = {} if ((lCat == 1 and unit1:isActive()) or lCat ~= 1) and unit1:isExist() == true then
unit_info1[#unit_info1].unit = unit1 unit_info1[#unit_info1 + 1] = {}
unit_info1[#unit_info1].pos = unit1:getPosition().p unit_info1[#unit_info1].unit = unit1
unit_info1[#unit_info1].pos = unit1:getPosition().p
end
end end
end end
for unitset2_ind = 1, #unitset2 do for unitset2_ind = 1, #unitset2 do
local unit2 = Unit.getByName(unitset2[unitset2_ind]) local unit2 = Unit.getByName(unitset2[unitset2_ind])
local lCat = Object.getCategory(unit2) if unit2 then
if unit2 and ((lCat == 1 and unit2:isActive()) or lCat ~= 1) and unit:isExist() == true then local lCat = Object.getCategory(unit2)
unit_info2[#unit_info2 + 1] = {} if ((lCat == 1 and unit2:isActive()) or lCat ~= 1) and unit2:isExist() == true then
unit_info2[#unit_info2].unit = unit2 unit_info2[#unit_info2 + 1] = {}
unit_info2[#unit_info2].pos = unit2:getPosition().p unit_info2[#unit_info2].unit = unit2
unit_info2[#unit_info2].pos = unit2:getPosition().p
end
end end
end end
@@ -4012,13 +4021,14 @@ do -- group functions scope
if Group.getByName(gpName) and Group.getByName(gpName):isExist() == true then if Group.getByName(gpName) and Group.getByName(gpName):isExist() == true then
local newGroup = Group.getByName(gpName) local newGroup = Group.getByName(gpName)
local newData = {} local newData = mist.utils.deepCopy(dbData)
newData.name = gpName newData.name = gpName
newData.groupId = tonumber(newGroup:getID()) newData.groupId = tonumber(newGroup:getID())
newData.category = newGroup:getCategory() newData.category = newGroup:getCategory()
newData.groupName = gpName newData.groupName = gpName
newData.hidden = dbData.hidden newData.hidden = dbData.hidden
if newData.category == 2 then if newData.category == 2 then
newData.category = 'vehicle' newData.category = 'vehicle'
elseif newData.category == 3 then 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. function mist.utils.getHeadingPoints(point1, point2, north) -- sick of writing this out.
if north then 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 else
return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1))) return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1)))
end 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') 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) --trigger.action.outText(errmsg, 10)
end end
end
end
--- Write debug data to file. --- Write debug data to file.
-- This function requires you to disable script sanitization -- This function requires you to disable script sanitization
-- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io
@@ -7653,7 +7664,10 @@ do
--log:warn(s) --log:warn(s)
if type(s) == 'table' then if type(s) == 'table' then
local mType = s.markType 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 if markScope == 'coa' then
trigger.action.markToCoalition(s.markId, s.text, s.pos, s.markFor, s.readOnly) trigger.action.markToCoalition(s.markId, s.text, s.pos, s.markFor, s.readOnly)
elseif markScope == 'group' then elseif markScope == 'group' then
@@ -7711,10 +7725,15 @@ do
local function validateColor(val) local function validateColor(val)
if type(val) == 'table' then if type(val) == 'table' then
for i = 1, #val do for i = 1, 4 do
if type(val[i]) == 'number' and val[i] > 1 then if val[i] then
val[i] = val[i]/255 -- convert RGB values from 0-255 to 0-1 equivilent. if type(val[i]) == 'number' and val[i] > 1 then
end 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 end
elseif type(val) == 'string' then elseif type(val) == 'string' then
val = mist.utils.hexToRGB(val) val = mist.utils.hexToRGB(val)
@@ -7755,7 +7774,7 @@ do
--log:info('create maker DB: $1', e.idx) --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} 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 if e.unit then
mist.DBs.markList[e.idx].unit = e.intiator:getName() mist.DBs.markList[e.idx].unit = e.initiator:getName()
end end
--log:info(mist.marker.list[e.idx]) --log:info(mist.marker.list[e.idx])
end end
@@ -7778,7 +7797,7 @@ do
else else
for mEntry, mData in pairs(mist.DBs.markList) do for mEntry, mData in pairs(mist.DBs.markList) do
if id == mData.name or id == mData.id then if id == mData.name or id == mData.id then
return mData.id return mData.markId
end end
end end
end end
@@ -7788,11 +7807,16 @@ do
local function removeMark(id) local function removeMark(id)
--log:info("Removing Mark: $1", id --log:info("Removing Mark: $1", id)
local removed = false local removed = false
if type(id) == 'table' then if type(id) == 'table' then
for ind, val in pairs(id) do 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 if r then
trigger.action.removeMark(r) trigger.action.removeMark(r)
mist.DBs.markList[r] = nil mist.DBs.markList[r] = nil
@@ -7802,9 +7826,11 @@ do
else else
local r = getMarkId(id) local r = getMarkId(id)
trigger.action.removeMark(r) if r then
mist.DBs.markList[r] = nil trigger.action.removeMark(r)
removed = true mist.DBs.markList[r] = nil
removed = true
end
end end
return removed return removed
end end
@@ -7926,6 +7952,7 @@ do
if markForCoa then if markForCoa then
if type(markForCoa) == 'string' then if type(markForCoa) == 'string' then
--log:warn("coa is string")
if tonumber(markForCoa) then if tonumber(markForCoa) then
coa = coas[tonumber(markForCoa)] coa = coas[tonumber(markForCoa)]
markScope = 'coa' markScope = 'coa'
@@ -7940,11 +7967,10 @@ do
end end
elseif type(markForCoa) == 'number' and markForCoa >=-1 and markForCoa <= #coas then elseif type(markForCoa) == 'number' and markForCoa >=-1 and markForCoa <= #coas then
coa = markForCoa coa = markForCoa
markScore = 'coa' --log:warn("coa is number")
markScope = 'coa'
end end
markFor = coa
elseif markFor then elseif markFor then
if type(markFor) == 'number' then -- groupId if type(markFor) == 'number' then -- groupId
if mist.DBs.groupsById[markFor] then if mist.DBs.groupsById[markFor] then
@@ -8053,7 +8079,7 @@ do
end end
for i = 1, #markForTable do for i = 1, #markForTable do
local newId = iterate() 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 mist.DBs.markList[newId] = data
table.insert(list, data) table.insert(list, data)
@@ -8177,6 +8203,7 @@ do
end end
function mist.marker.remove(id) function mist.marker.remove(id)
return removeMark(id) return removeMark(id)
end end
@@ -8967,8 +8994,8 @@ do -- group tasks scope
minR = mist.utils.get2DDist(avg, zone[i]) minR = mist.utils.get2DDist(avg, zone[i])
end end
end end
--log:warn('Radius: $1', radius)
--log:warn('minR: $1', minR) --log:warn('minR: $1', minR)
--log:warn('Radius: $1', radius)
local lSpawnPos = {} local lSpawnPos = {}
for j = 1, 100 do for j = 1, 100 do
newCoord = mist.getRandPointInCircle(avg, radius) newCoord = mist.getRandPointInCircle(avg, radius)
@@ -9200,7 +9227,7 @@ do -- group tasks scope
function mist.groupIsDead(groupName) -- copy more or less from on station function mist.groupIsDead(groupName) -- copy more or less from on station
local gp = Group.getByName(groupName) local gp = Group.getByName(groupName)
if gp then if gp then
if #gp:getUnits() > 0 or gp:isExist() == true then if #gp:getUnits() > 0 and gp:isExist() == true then
return false return false
end end
end end

View File

@@ -5,18 +5,58 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Voice control", "name": "Python: Main",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "voice_control.py", "program": "${workspaceFolder}/main.py",
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": false, "justMyCode": false,
}, },
{ {
"name": "Test bed", "name": "Example voice control",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "testbed.py", "program": "example_voice_control.py",
"console": "integratedTerminal",
"justMyCode": false,
},
{
"name": "Example disembarked infantry",
"type": "debugpy",
"request": "launch",
"program": "example_disembarked_infantry.py",
"console": "integratedTerminal",
"justMyCode": false,
},
{
"name": "Example set cargo weight",
"type": "debugpy",
"request": "launch",
"program": "example_set_cargo_weight.py",
"console": "integratedTerminal",
"justMyCode": false,
},
{
"name": "Example draw argument",
"type": "debugpy",
"request": "launch",
"program": "example_draw_argument.py",
"console": "integratedTerminal",
"justMyCode": false,
},
{
"name": "Example precise movement",
"type": "debugpy",
"request": "launch",
"program": "example_precise_movement.py",
"console": "integratedTerminal",
"justMyCode": false,
},
{
"name": "Infantry boarding",
"type": "debugpy",
"request": "launch",
"program": "infantry_boarding.py",
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": false, "justMyCode": false,
} }

View File

@@ -120,6 +120,121 @@ class API:
if hasattr(signal, 'SIGTERM'): if hasattr(signal, 'SIGTERM'):
signal.signal(signal.SIGTERM, signal_handler) # Termination signal 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): 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. Get all units from the API. Notice that if the API is not running, update_units() must be manually called first.
@@ -171,7 +286,6 @@ class API:
else: else:
self.logger.error(f"Failed to fetch units: {response.status_code} - {response.text}") self.logger.error(f"Failed to fetch units: {response.status_code} - {response.text}")
def update_logs(self, time = 0): def update_logs(self, time = 0):
""" """
Fetch the logs from the API. Fetch the logs from the API.
@@ -192,7 +306,7 @@ class API:
else: else:
self.logger.error(f"Failed to fetch logs: {response.status_code} - {response.text}") self.logger.error(f"Failed to fetch logs: {response.status_code} - {response.text}")
def spawn_aircrafts(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0): def spawn_aircrafts(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0, execution_callback=None):
""" """
Spawn aircraft units at the specified location or airbase. Spawn aircraft units at the specified location or airbase.
Args: Args:
@@ -202,6 +316,7 @@ class API:
country (str): The country of the units. country (str): The country of the units.
immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler. immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler.
spawnPoints (int): Amount of spawn points to use, default is 0. 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 = { command = {
"units": [unit.toJSON() for unit in units], "units": [unit.toJSON() for unit in units],
@@ -214,7 +329,21 @@ class API:
data = { "spawnAircrafts": command } data = { "spawnAircrafts": command }
response = self._put(data) response = self._put(data)
def spawn_helicopters(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0): # Parse the response as JSON if callback is provided
if execution_callback:
try:
response_data = response.json()
command_hash = response_data.get("commandHash", None)
if command_hash:
self.logger.info(f"Aircraft spawned successfully. Command Hash: {command_hash}")
# Start a background task to check if the command was executed
asyncio.create_task(self._check_command_executed(command_hash, execution_callback, wait_for_result=True))
else:
self.logger.error("Command hash not found in response")
except ValueError:
self.logger.error("Failed to parse JSON response")
def spawn_helicopters(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0, execution_callback=None):
""" """
Spawn helicopter units at the specified location or airbase. Spawn helicopter units at the specified location or airbase.
Args: Args:
@@ -224,6 +353,7 @@ class API:
country (str): The country of the units. country (str): The country of the units.
immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler. immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler.
spawnPoints (int): Amount of spawn points to use, default is 0. 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 = { command = {
"units": [unit.toJSON() for unit in units], "units": [unit.toJSON() for unit in units],
@@ -236,6 +366,20 @@ class API:
data = { "spawnHelicopters": command } data = { "spawnHelicopters": command }
response = self._put(data) 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): 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. Spawn ground units at the specified location.
@@ -272,32 +416,7 @@ class API:
except ValueError: except ValueError:
self.logger.error("Failed to parse JSON response") self.logger.error("Failed to parse JSON response")
async def _check_command_executed(self, command_hash: str, execution_callback, wait_for_result: bool, max_wait_time: int = 60): def spawn_navy_units(self, units: list[UnitSpawnTable], coalition: str, country: str, immediate: bool, spawnPoints: int = 0, execution_callback=None):
"""
Check if a command has been executed by polling the API.
"""
start_time = time.time()
while True:
response = self._get(f"commands?commandHash={command_hash}")
if response.status_code == 200:
try:
data = response.json()
if data.get("commandExecuted") == True and (data.get("commandResult") is not None or (not wait_for_result)):
self.logger.info(f"Command {command_hash} executed successfully, command result: {data.get('commandResult')}")
if execution_callback:
await execution_callback(data.get("commandResult"))
break
elif data.get("status") == "failed":
self.logger.error(f"Command {command_hash} failed to execute.")
break
except ValueError:
self.logger.error("Failed to parse JSON response")
if time.time() - start_time > max_wait_time:
self.logger.warning(f"Timeout: Command {command_hash} did not complete within {max_wait_time} seconds.")
break
await asyncio.sleep(1)
def spawn_navy_units(self, units: list[UnitSpawnTable], coalition: str, country: str, immediate: bool, spawnPoints: int = 0):
""" """
Spawn navy units at the specified location. Spawn navy units at the specified location.
Args: Args:
@@ -306,6 +425,7 @@ class API:
country (str): The country of the units. country (str): The country of the units.
immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler. immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler.
spawnPoints (int): Amount of spawn points to use, default is 0. 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 = { command = {
"units": [unit.toJSON() for unit in units], "units": [unit.toJSON() for unit in units],
@@ -317,6 +437,20 @@ class API:
data = { "spawnNavyUnits": command } data = { "spawnNavyUnits": command }
response = self._put(data) 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): def create_radio_listener(self):
""" """
Create an audio listener instance. Create an audio listener instance.
@@ -327,55 +461,6 @@ class API:
from radio.radio_listener import RadioListener from radio.radio_listener import RadioListener
return RadioListener(self, "localhost", self.config.get("audio").get("WSPort")) return RadioListener(self, "localhost", self.config.get("audio").get("WSPort"))
def register_on_update_callback(self, callback):
"""
Register a callback function to be called on each update.
Args:
callback (function): The function to call on update. Can be sync or async.
The function should accept a single argument, which is the API instance.
"""
self.on_update_callback = callback
def register_on_startup_callback(self, callback):
"""
Register a callback function to be called on startup.
Args:
callback (function): The function to call on startup. Can be sync or async.
The function should accept a single argument, which is the API instance.
"""
self.on_startup_callback = callback
def set_log_level(self, level):
"""
Set the logging level for the API.
Args:
level: Logging level (e.g., logging.DEBUG, logging.INFO, logging.WARNING, self.logger.error)
"""
self.logger.setLevel(level)
self.logger.info(f"Log level set to {logging.getLevelName(level)}")
def stop(self):
"""
Stop the API service gracefully.
"""
self.logger.info("Stopping API service...")
self.should_stop = True
async def _run_callback_async(self, callback, *args):
"""
Run a callback asynchronously, handling both sync and async callbacks.
"""
try:
if asyncio.iscoroutinefunction(callback):
await callback(*args)
else:
callback(*args)
except Exception as e:
# Log the error but don't crash the update process
self.logger.error(f"Error in callback: {e}")
def generate_audio_message(text: str, gender: str = "male", code: str = "en-US") -> str: 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. Generate a WAV file from text using Google Text-to-Speech API.
@@ -412,28 +497,6 @@ class API:
return file_name return file_name
def send_command(self, command: str):
"""
Send a command to the API.
Args:
command (str): The command to send.
"""
response = self._put(command)
if response.status_code == 200:
self.logger.info(f"Command sent successfully: {command}")
else:
self.logger.error(f"Failed to send command: {response.status_code} - {response.text}")
def run(self):
"""
Start the API service.
This method initializes the API and starts the necessary components.
Sets up signal handlers for graceful shutdown.
"""
asyncio.run(self._run_async())
def get_closest_units(self, coalitions: list[str], categories: list[str], position: LatLng, operate_as: str | None = None, max_number: int = 1, max_distance: float = 10000) -> list[Unit]: 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. Get the closest units of a specific coalition and category to a given position.
@@ -453,7 +516,7 @@ class API:
# Iterate through all units and find the closest ones that match the criteria # Iterate through all units and find the closest ones that match the criteria
for unit in self.units.values(): for unit in self.units.values():
if unit.alive and unit.coalition in coalitions and unit.category.lower() in categories and (operate_as is None or unit.operate_as == operate_as or unit.coalition is not "neutral"): if unit.alive and unit.coalition in coalitions and unit.category.lower() in categories and (operate_as is None or unit.operate_as == operate_as or unit.coalition != "neutral"):
distance = position.distance_to(unit.position) distance = position.distance_to(unit.position)
if distance < closest_distance: if distance < closest_distance:
closest_distance = distance closest_distance = distance
@@ -469,38 +532,32 @@ class API:
return closest_units return closest_units
async def _run_async(self): def send_command(self, command: str):
""" """
Async implementation of the API service loop. Send a command to the API.
Args:
command (str): The command to send.
""" """
# Setup signal handlers for graceful shutdown response = self._put(command)
self._setup_signal_handlers() 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}")
# Here you can add any initialization logic if needed def stop(self):
self.logger.info("API started") """
self.logger.info("Press Ctrl+C to stop gracefully") Stop the API service gracefully.
"""
self.logger.info("Stopping API service...")
self.should_stop = True
self.running = True def run(self):
self.should_stop = False """
Start the API service.
# Call the startup callback if registered This method initializes the API and starts the necessary components.
if self.on_startup_callback: Sets up signal handlers for graceful shutdown.
try: """
await self._run_callback_async(self.on_startup_callback, self) asyncio.run(self._run_async())
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

View File

@@ -1,6 +1,6 @@
import struct import struct
from typing import List from typing import List
from data.data_types import LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset from data.data_types import DrawArgument, LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset
class DataExtractor: class DataExtractor:
def __init__(self, buffer: bytes): def __init__(self, buffer: bytes):
@@ -48,6 +48,7 @@ class DataExtractor:
lat = self.extract_float64() lat = self.extract_float64()
lng = self.extract_float64() lng = self.extract_float64()
alt = self.extract_float64() alt = self.extract_float64()
threshold = self.extract_float64()
return LatLng(lat, lng, alt) return LatLng(lat, lng, alt)
def extract_from_bitmask(self, bitmask: int, position: int) -> bool: def extract_from_bitmask(self, bitmask: int, position: int) -> bool:
@@ -137,3 +138,13 @@ class DataExtractor:
y=self.extract_float64(), y=self.extract_float64(),
z=self.extract_float64() z=self.extract_float64()
) )
def extract_draw_arguments(self) -> List[DrawArgument]:
value = []
size = self.extract_uint16()
for _ in range(size):
value.append(DrawArgument(
argument=self.extract_uint32(),
value=self.extract_float64()
))
return value

View File

@@ -67,4 +67,8 @@ class DataIndexes(Enum):
AIM_METHOD_RANGE = 63 AIM_METHOD_RANGE = 63
ACQUISITION_RANGE = 64 ACQUISITION_RANGE = 64
AIRBORNE = 65 AIRBORNE = 65
CARGO_WEIGHT = 66
DRAW_ARGUMENTS = 67
CUSTOM_STRING = 68
CUSTOM_INTEGER = 69
END_OF_DATA = 255 END_OF_DATA = 255

View File

@@ -8,13 +8,15 @@ class LatLng:
lat: float lat: float
lng: float lng: float
alt: float alt: float
threshold: Optional[float] = 0 # Optional threshold for proximity checks
def toJSON(self): def toJSON(self):
"""Convert LatLng to a JSON serializable dictionary.""" """Convert LatLng to a JSON serializable dictionary."""
return { return {
"lat": self.lat, "lat": self.lat,
"lng": self.lng, "lng": self.lng,
"alt": self.alt "alt": self.alt,
"threshold": self.threshold
} }
def project_with_bearing_and_distance(self, d, bearing): def project_with_bearing_and_distance(self, d, bearing):
@@ -89,3 +91,8 @@ class Offset:
x: float x: float
y: float y: float
z: float z: float
@dataclass
class DrawArgument:
argument: int
value: float

View File

@@ -5,14 +5,14 @@ from math import pi
# Setup a logger for the module # Setup a logger for the module
import logging import logging
logger = logging.getLogger("TestBed") logger = logging.getLogger("example_disembarked_infantry")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
handler = logging.StreamHandler() handler = logging.StreamHandler()
formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s') formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
units_to_delete = None units_to_delete = []
############################################################################################# #############################################################################################
# This class represents a disembarked infantry unit that will engage in combat # This class represents a disembarked infantry unit that will engage in combat
@@ -86,7 +86,7 @@ class DisembarkedInfantry(Unit):
# Simulate a firefight in the direction of the enemy # Simulate a firefight in the direction of the enemy
firefight_destination = self.position.project_with_bearing_and_distance(30, bearing_to_enemy) firefight_destination = self.position.project_with_bearing_and_distance(30, bearing_to_enemy)
self.simulate_fire_fight(firefight_destination.lat, firefight_destination.lng, firefight_destination.alt + 1) self.simulate_fire_fight(firefight_destination, firefight_destination.alt + 1)
await asyncio.sleep(10) # Simulate some time spent in firefight await asyncio.sleep(10) # Simulate some time spent in firefight
self.start_fighting() # Restart the fighting process self.start_fighting() # Restart the fighting process
@@ -109,8 +109,8 @@ def on_api_startup(api: API):
if unit.alive and not unit.human and unit.coalition == "blue": if unit.alive and not unit.human and unit.coalition == "blue":
units_to_delete.append(unit) units_to_delete.append(unit)
try: try:
unit.delete_unit(False, "", True)
unit.register_on_property_change_callback("alive", on_unit_alive_change) unit.register_on_property_change_callback("alive", on_unit_alive_change)
unit.delete_unit(False, "", True)
logger.info(f"Deleted unit: {unit}") logger.info(f"Deleted unit: {unit}")
except Exception as e: except Exception as e:
@@ -175,7 +175,7 @@ def on_api_update(api: API):
new_unit.__class__ = DisembarkedInfantry new_unit.__class__ = DisembarkedInfantry
new_unit.start_fighting() new_unit.start_fighting()
api.spawn_ground_units([spawn_table], unit.coalition, "", True, 0, lambda new_group_ID: execution_callback(new_group_ID)) api.spawn_ground_units([spawn_table], unit.coalition, "", True, 0, execution_callback)
logger.info(f"Spawned new unit succesfully at {spawn_position} with heading {unit.heading}") logger.info(f"Spawned new unit succesfully at {spawn_position} with heading {unit.heading}")
break break

View File

@@ -0,0 +1,31 @@
from api import API
def on_api_startup(api: API):
units = api.update_units()
for unit in units.values():
if unit.name == "UH-1H":
# Register draw argument 43 for UH-1H
unit.register_draw_argument(43)
def on_api_update(api: API):
units = api.get_units()
for unit in units.values():
if unit.name == "UH-1H":
print(f"Draw Arguments for {unit.name}:")
for draw_arg in unit.draw_arguments:
print(f" Argument: {draw_arg.argument}, Value: {draw_arg.value}")
##############################################################################################
# Main entry point for the script. It registers the callbacks and starts the API.
##############################################################################################
if __name__ == "__main__":
# Initialize the API
api = API()
# Register the callbacks
api.register_on_update_callback(on_api_update)
api.register_on_startup_callback(on_api_startup)
# Start the API, this will run forever until stopped
api.run()

View File

@@ -0,0 +1,24 @@
from api import API
def on_api_startup(api: API):
units = api.update_units()
for unit in units.values():
if unit.name == "Infantry AK Ins":
current_pos = unit.position
next_pos = current_pos.project_with_bearing_and_distance(20, 0) # Move 20 meters north
next_pos.threshold = 2 # Set threshold to 1 meter, very precise
unit.set_path([next_pos])
##############################################################################################
# Main entry point for the script. It registers the callbacks and starts the API.
##############################################################################################
if __name__ == "__main__":
# Initialize the API
api = API()
# Register the callbacks
api.register_on_startup_callback(on_api_startup)
# Start the API, this will run forever until stopped
api.run()

View File

@@ -0,0 +1,29 @@
from api import API
def on_api_startup(api: API):
units = api.update_units()
for unit in units.values():
if unit.name == "UH-1H":
# Set cargo weight to 5000 kg
unit.set_cargo_weight(5000.0)
def on_api_update(api: API):
units = api.get_units()
for unit in units.values():
if unit.name == "UH-1H":
print(f"Cargo Weight for {unit.name}: {unit.cargo_weight} kg")
##############################################################################################
# Main entry point for the script. It registers the callbacks and starts the API.
##############################################################################################
if __name__ == "__main__":
# Initialize the API
api = API()
# Register the callbacks
api.register_on_update_callback(on_api_update)
api.register_on_startup_callback(on_api_startup)
# Start the API, this will run forever until stopped
api.run()

View File

@@ -1,12 +1,13 @@
from math import pi from math import pi
import os
from api import API, UnitSpawnTable from api import API, UnitSpawnTable
from radio.radio_listener import RadioListener from radio.radio_listener import RadioListener
# Setup a logger for the module # Setup a logger for the module
import logging import logging
logger = logging.getLogger("OlympusVoiceControl") logger = logging.getLogger("example_voice_control")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
handler = logging.StreamHandler() handler = logging.StreamHandler()
formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s') formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s')
@@ -70,12 +71,15 @@ def on_message_received(recognized_text: str, unit_id: str, api: API, listener:
message_filename = api.generate_audio_message("I did not understand") message_filename = api.generate_audio_message("I did not understand")
listener.transmit_on_frequency(message_filename, listener.frequency, listener.modulation, listener.encryption) 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__": if __name__ == "__main__":
api = API() api = API()
logger.info("API initialized") logger.info("API initialized")
listener = api.create_radio_listener() listener = api.create_radio_listener()
listener.start(frequency=251.000e6, modulation=0, encryption=0) listener.start(frequency=251.000e6, modulation=0, encryption=0)
listener.register_message_callback(lambda wav_filename, unit_id, api=api, listener=listener: on_message_received(wav_filename, unit_id, api, listener)) listener.register_message_callback(lambda recognized_text, unit_id, api=api, listener=listener: on_message_received(recognized_text, unit_id, api, listener))
api.run() api.run()

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

View File

@@ -1,18 +0,0 @@
import re
# Read the file
with open('unit.py', 'r', encoding='utf-8') as f:
content = f.read()
# Pattern to match callback invocations
pattern = r'self\.on_property_change_callbacks\[\"(\w+)\"\]\(self, self\.(\w+)\)'
replacement = r'self._trigger_callback("\1", self.\2)'
# Replace all matches
new_content = re.sub(pattern, replacement, content)
# Write back to file
with open('unit.py', 'w', encoding='utf-8') as f:
f.write(new_content)
print('Updated all callback invocations')

View File

@@ -3,7 +3,7 @@ import asyncio
from data.data_extractor import DataExtractor from data.data_extractor import DataExtractor
from data.data_indexes import DataIndexes from data.data_indexes import DataIndexes
from data.data_types import LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset from data.data_types import DrawArgument, LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset
from data.roes import ROES from data.roes import ROES
from data.states import states from data.states import states
from utils.utils import enum_to_coalition from utils.utils import enum_to_coalition
@@ -81,6 +81,10 @@ class Unit:
self.targeting_range = 0.0 self.targeting_range = 0.0
self.aim_method_range = 0.0 self.aim_method_range = 0.0
self.acquisition_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.previous_total_ammo = 0
self.total_ammo = 0 self.total_ammo = 0
@@ -654,6 +658,34 @@ class Unit:
# Trigger callbacks for property change # Trigger callbacks for property change
if "airborne" in self.on_property_change_callbacks: if "airborne" in self.on_property_change_callbacks:
self._trigger_callback("airborne", self.airborne) 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 --- # --- API functions requiring ID ---
def set_path(self, path: List[LatLng]): def set_path(self, path: List[LatLng]):
@@ -758,6 +790,14 @@ class Unit:
def set_engagement_properties(self, barrel_height, muzzle_velocity, aim_time, shots_to_fire, shots_base_interval, shots_base_scatter, engagement_range, targeting_range, aim_method_range, acquisition_range): 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}}) return self.api.send_command({"setEngagementProperties": {"ID": self.ID, "barrelHeight": barrel_height, "muzzleVelocity": muzzle_velocity, "aimTime": aim_time, "shotsToFire": shots_to_fire, "shotsBaseInterval": shots_base_interval, "shotsBaseScatter": shots_base_scatter, "engagementRange": engagement_range, "targetingRange": targeting_range, "aimMethodRange": aim_method_range, "acquisitionRange": acquisition_range}})
def set_cargo_weight(self, cargo_weight: float):
return self.api.send_command({"setCargoWeight": {"ID": self.ID, "weight": cargo_weight}})
def register_draw_argument(self, argument: int, active: bool = True):
return self.api.send_command({"registerDrawArgument": {"ID": self.ID, "argument": argument, "active": active}})
def set_custom_string(self, custom_string: str):
return self.api.send_command({"setCustomString": {"ID": self.ID, "customString": custom_string}})
def set_custom_integer(self, custom_integer: int):
return self.api.send_command({"setCustomInteger": {"ID": self.ID, "customInteger": custom_integer}})

View File

@@ -4,8 +4,6 @@ import inspect
import difflib import difflib
from slpp import slpp as lua from slpp import slpp as lua
SEARCH_FOLDER = sys.argv[2]
from dcs.weapons_data import Weapons from dcs.weapons_data import Weapons
from dcs.planes import * from dcs.planes import *
from dcs.helicopters import * from dcs.helicopters import *
@@ -122,9 +120,6 @@ if len(sys.argv) > 1:
with open('payloadRoles.json') as f: with open('payloadRoles.json') as f:
payloads_roles = json.load(f) payloads_roles = json.load(f)
with open('pylonUsage.json') as f:
pylon_usage = json.load(f)
# Loop on all the units in the database # Loop on all the units in the database
for unit_name in database: for unit_name in database:
try: try:
@@ -133,10 +128,6 @@ if len(sys.argv) > 1:
unitmap = plane_map unitmap = plane_map
elif (sys.argv[1] == "helicopter"): elif (sys.argv[1] == "helicopter"):
unitmap = helicopter_map 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()] lowercase_keys = [key.lower() for key in unitmap.keys()]
res = difflib.get_close_matches(unit_name.lower(), lowercase_keys) res = difflib.get_close_matches(unit_name.lower(), lowercase_keys)
if len(res) > 0: if len(res) > 0:
@@ -157,14 +148,25 @@ if len(sys.argv) > 1:
} }
database[unit_name]["loadouts"].append(empty_loadout) 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 # Add the available pylon usage
database[unit_name]["acceptedPayloads"] = {} database[unit_name]["acceptedPayloads"] = {}
for pylon_name in pylon_usage[unit_name]: for pylon_name in pylon_usage:
pylon_data = pylon_usage[unit_name][pylon_name] pylon_data = pylon_usage[pylon_name]
database[unit_name]["acceptedPayloads"][pylon_name] = { database[unit_name]["acceptedPayloads"][pylon_name] = pylon_usage[pylon_name]
"clsids": pylon_data,
"names": [find_weapon_name(clsid) for clsid in pylon_data]
}
# Loop on all the loadouts for that unit # Loop on all the loadouts for that unit
for payload_name in unit_payloads[unit_name]: for payload_name in unit_payloads[unit_name]:

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

View 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"
}

View File

@@ -3774,20 +3774,73 @@
} }
}, },
"F4U-1D": { "F4U-1D": {
"Drop tank 175 US gal.": {
"1": 11,
"2": 18,
"3": 19,
"4": 17
},
"HVAR x 8": { "HVAR x 8": {
"1": 32, "1": 31
"2": 31,
"3": 30
}, },
"Bomb x 2, HVAR x 4": { "M-64 bomb x 2, HVAR x 8": {
"1": 32, "1": 31
"2": 31,
"3": 30
}, },
"Tiny Tim x2, HVAR x 4": { "M-64 bomb x 3, HVAR x 6": {
"1": 32, "1": 31
"2": 31, },
"3": 30 "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": { "F/A-18A": {
@@ -4742,6 +4795,209 @@
}, },
"AEROBATIC": {} "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": { "Mirage-F1B": {
"Clean": { "Clean": {
"1": 10, "1": 10,
@@ -6437,6 +6693,125 @@
"1": 18 "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": { "B-1B": {
"Mk-82*84": { "Mk-82*84": {
"1": 34, "1": 34,
@@ -7625,120 +8000,6 @@
} }
}, },
"Mi-26": {}, "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": { "Mi-8MT": {
"4 x B8": { "4 x B8": {
"1": 32 "1": 32
@@ -9158,11 +9419,6 @@
"2": 34 "2": 34
} }
}, },
"Tu-95MS": {
"Kh-65*6": {
"1": 33
}
},
"UH-1H": { "UH-1H": {
"M134 Minigun*2, XM158*2": { "M134 Minigun*2, XM158*2": {
"1": 32, "1": 32,

View File

@@ -1939,6 +1939,11 @@
] ]
}, },
"F4U-1D": { "F4U-1D": {
"6": [
"{175_USgal_Corsair_droptank_aux}",
"{AN-M64}",
"{ASM_N_2}"
],
"1": [ "1": [
"{HVAR_USN_Mk28_Mod4_Corsair}" "{HVAR_USN_Mk28_Mod4_Corsair}"
], ],
@@ -1951,9 +1956,47 @@
"4": [ "4": [
"{HVAR_USN_Mk28_Mod4_Corsair}" "{HVAR_USN_Mk28_Mod4_Corsair}"
], ],
"11": [
"{HVAR_USN_Mk28_Mod4_Corsair}"
],
"10": [
"{HVAR_USN_Mk28_Mod4_Corsair}"
],
"9": [
"{HVAR_USN_Mk28_Mod4_Corsair}"
],
"8": [ "8": [
"{HVAR_USN_Mk28_Mod4_Corsair}" "{HVAR_USN_Mk28_Mod4_Corsair}"
], ],
"5": [
"{AN-M64}",
"{AN_M65}",
"{Tiny_Tim_Corsair_L}"
],
"7": [
"{AN-M64}",
"{AN_M65}",
"{Tiny_Tim_Corsair_R}"
]
},
"F4U-1D_CW": {
"6": [
"{175_USgal_Corsair_droptank_aux}",
"{AN-M64}",
"{ASM_N_2}"
],
"1": [
"{HVAR_USN_Mk28_Mod4_Corsair}"
],
"2": [
"{HVAR_USN_Mk28_Mod4_Corsair}"
],
"3": [
"{HVAR_USN_Mk28_Mod4_Corsair}"
],
"4": [
"{HVAR_USN_Mk28_Mod4_Corsair}"
],
"11": [ "11": [
"{HVAR_USN_Mk28_Mod4_Corsair}" "{HVAR_USN_Mk28_Mod4_Corsair}"
], ],
@@ -1963,13 +2006,18 @@
"9": [ "9": [
"{HVAR_USN_Mk28_Mod4_Corsair}" "{HVAR_USN_Mk28_Mod4_Corsair}"
], ],
"7": [ "8": [
"{AN-M64}", "{HVAR_USN_Mk28_Mod4_Corsair}"
"{Tiny_Tim_Corsair}"
], ],
"5": [ "5": [
"{AN-M64}", "{AN-M64}",
"{Tiny_Tim_Corsair}" "{AN_M65}",
"{Tiny_Tim_Corsair_L}"
],
"7": [
"{AN-M64}",
"{AN_M65}",
"{Tiny_Tim_Corsair_R}"
] ]
}, },
"F/A-18A": { "F/A-18A": {
@@ -2718,6 +2766,103 @@
"{SMOKE_WHITE}" "{SMOKE_WHITE}"
] ]
}, },
"MiG-29 Fulcrum": {
"1": [
"{FBC29BFE-3D24-4C64-B81D-941239D12249}",
"{682A481F-0CB5-4693-A382-D00DD4A156D7}",
"{R_60}"
],
"2": [
"{FBC29BFE-3D24-4C64-B81D-941239D12249}",
"{682A481F-0CB5-4693-A382-D00DD4A156D7}",
"{R_60}",
"{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}",
"B-8M1 - 20 S-8OFP2",
"{F72F47E5-C83A-4B85-96ED-D3E46671EE9A}",
"{35B698AC-9FEF-4EC4-AD29-484A0085F62B}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{4203753F-8198-4E85-9924-6F8FF679F9FF}",
"{RBK_250_275_AO_1SCH}",
"{7AEC222D-C523-425e-B714-719C0D1EB14D}",
"{D5435F26-F120-4FA3-9867-34ACE562EF1B}",
"{FAB_250_M62}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"{BD289E34-DF84-4C5E-9220-4B14C346E79D}",
"{3DFB7320-AB0E-11d7-9897-000476191836}"
],
"3": [
"{88DAC840-9F75-4531-8689-B46E64E42E53}",
"{9B25D316-0434-4954-868F-D51DB1A38DF0}",
"{FBC29BFE-3D24-4C64-B81D-941239D12249}",
"{R_60}",
"{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}",
"B-8M1 - 20 S-8OFP2",
"{F72F47E5-C83A-4B85-96ED-D3E46671EE9A}",
"{35B698AC-9FEF-4EC4-AD29-484A0085F62B}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{96A7F676-F956-404A-AD04-F33FB2C74881}",
"{4203753F-8198-4E85-9924-6F8FF679F9FF}",
"{RBK_250_275_AO_1SCH}",
"{7AEC222D-C523-425e-B714-719C0D1EB14D}",
"{D5435F26-F120-4FA3-9867-34ACE562EF1B}",
"{FAB_250_M62}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"{BD289E34-DF84-4C5E-9220-4B14C346E79D}",
"{3DFB7320-AB0E-11d7-9897-000476191836}",
"{682A481F-0CB5-4693-A382-D00DD4A156D7}",
"{E8069896-8435-4B90-95C0-01A03AE6E400}",
"{B79C379A-9E87-4E50-A1EE-7F7E29C2E87A}"
],
"5": [
"{88DAC840-9F75-4531-8689-B46E64E42E53}",
"{9B25D316-0434-4954-868F-D51DB1A38DF0}",
"{FBC29BFE-3D24-4C64-B81D-941239D12249}",
"{R_60}",
"{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}",
"B-8M1 - 20 S-8OFP2",
"{F72F47E5-C83A-4B85-96ED-D3E46671EE9A}",
"{35B698AC-9FEF-4EC4-AD29-484A0085F62B}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{96A7F676-F956-404A-AD04-F33FB2C74881}",
"{4203753F-8198-4E85-9924-6F8FF679F9FF}",
"{RBK_250_275_AO_1SCH}",
"{7AEC222D-C523-425e-B714-719C0D1EB14D}",
"{D5435F26-F120-4FA3-9867-34ACE562EF1B}",
"{FAB_250_M62}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"{BD289E34-DF84-4C5E-9220-4B14C346E79D}",
"{3DFB7320-AB0E-11d7-9897-000476191836}",
"{682A481F-0CB5-4693-A382-D00DD4A156D7}",
"{E8069896-8435-4B90-95C0-01A03AE6E400}",
"{B79C379A-9E87-4E50-A1EE-7F7E29C2E87A}"
],
"6": [
"{FBC29BFE-3D24-4C64-B81D-941239D12249}",
"{682A481F-0CB5-4693-A382-D00DD4A156D7}",
"{R_60}",
"{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}",
"B-8M1 - 20 S-8OFP2",
"{F72F47E5-C83A-4B85-96ED-D3E46671EE9A}",
"{35B698AC-9FEF-4EC4-AD29-484A0085F62B}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{4203753F-8198-4E85-9924-6F8FF679F9FF}",
"{RBK_250_275_AO_1SCH}",
"{7AEC222D-C523-425e-B714-719C0D1EB14D}",
"{D5435F26-F120-4FA3-9867-34ACE562EF1B}",
"{FAB_250_M62}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"{BD289E34-DF84-4C5E-9220-4B14C346E79D}",
"{3DFB7320-AB0E-11d7-9897-000476191836}"
],
"7": [
"{FBC29BFE-3D24-4C64-B81D-941239D12249}",
"{682A481F-0CB5-4693-A382-D00DD4A156D7}",
"{R_60}"
],
"4": [
"{PTB_1500_MIG29A}"
]
},
"Mirage-F1B": { "Mirage-F1B": {
"1": [ "1": [
"<CLEAN>", "<CLEAN>",
@@ -4365,6 +4510,59 @@
"{FAS}" "{FAS}"
] ]
}, },
"Mi-28N": {
"2": [
"{3C612111-C7AD-476E-8A8E-2485812F4E5C}",
"{PTB_450}",
"{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{05544F1A-C39C-466b-BC37-5BD1D52E57BB}",
"{96A7F676-F956-404A-AD04-F33FB2C74881}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"B_8V20A_CM",
"{FC56DF80-9B09-44C5-8976-DCFAFF219062}"
],
"3": [
"{3C612111-C7AD-476E-8A8E-2485812F4E5C}",
"{PTB_450}",
"{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{05544F1A-C39C-466b-BC37-5BD1D52E57BB}",
"{96A7F676-F956-404A-AD04-F33FB2C74881}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"B_8V20A_CM",
"{FC56DF80-9B09-44C5-8976-DCFAFF219062}"
],
"1": [
"{PTB_450}",
"{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{05544F1A-C39C-466b-BC37-5BD1D52E57BB}",
"{57232979-8B0F-4db7-8D9A-55197E06B0F5}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"{FC56DF80-9B09-44C5-8976-DCFAFF219062}",
"{3C612111-C7AD-476E-8A8E-2485812F4E5C}",
"{96A7F676-F956-404A-AD04-F33FB2C74881}",
"B_8V20A_CM"
],
"4": [
"{PTB_450}",
"{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{05544F1A-C39C-466b-BC37-5BD1D52E57BB}",
"{57232979-8B0F-4db7-8D9A-55197E06B0F5}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"{FC56DF80-9B09-44C5-8976-DCFAFF219062}",
"{3C612111-C7AD-476E-8A8E-2485812F4E5C}",
"{96A7F676-F956-404A-AD04-F33FB2C74881}",
"B_8V20A_CM"
]
},
"Tu-95MS": {
"1": [
"{0290F5DE-014A-4BB1-9843-D717749B1DED}"
]
},
"B-1B": { "B-1B": {
"1": [ "1": [
"MK_82*28", "MK_82*28",
@@ -5365,54 +5563,6 @@
] ]
}, },
"Mi-26": {}, "Mi-26": {},
"Mi-28N": {
"2": [
"{3C612111-C7AD-476E-8A8E-2485812F4E5C}",
"{PTB_450}",
"{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{05544F1A-C39C-466b-BC37-5BD1D52E57BB}",
"{96A7F676-F956-404A-AD04-F33FB2C74881}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"B_8V20A_CM",
"{FC56DF80-9B09-44C5-8976-DCFAFF219062}"
],
"3": [
"{3C612111-C7AD-476E-8A8E-2485812F4E5C}",
"{PTB_450}",
"{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{05544F1A-C39C-466b-BC37-5BD1D52E57BB}",
"{96A7F676-F956-404A-AD04-F33FB2C74881}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"B_8V20A_CM",
"{FC56DF80-9B09-44C5-8976-DCFAFF219062}"
],
"1": [
"{PTB_450}",
"{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{05544F1A-C39C-466b-BC37-5BD1D52E57BB}",
"{57232979-8B0F-4db7-8D9A-55197E06B0F5}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"{FC56DF80-9B09-44C5-8976-DCFAFF219062}",
"{3C612111-C7AD-476E-8A8E-2485812F4E5C}",
"{96A7F676-F956-404A-AD04-F33FB2C74881}",
"B_8V20A_CM"
],
"4": [
"{PTB_450}",
"{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}",
"{96A7F676-F956-404A-AD04-F33FB2C74884}",
"{05544F1A-C39C-466b-BC37-5BD1D52E57BB}",
"{57232979-8B0F-4db7-8D9A-55197E06B0F5}",
"{37DCC01E-9E02-432F-B61D-10C166CA2798}",
"{FC56DF80-9B09-44C5-8976-DCFAFF219062}",
"{3C612111-C7AD-476E-8A8E-2485812F4E5C}",
"{96A7F676-F956-404A-AD04-F33FB2C74881}",
"B_8V20A_CM"
]
},
"Mi-8MT": { "Mi-8MT": {
"5": [ "5": [
"{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}", "{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}",
@@ -7042,11 +7192,6 @@
"{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}" "{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}"
] ]
}, },
"Tu-95MS": {
"1": [
"{0290F5DE-014A-4BB1-9843-D717749B1DED}"
]
},
"UH-1H": { "UH-1H": {
"1": [ "1": [
"M134_L" "M134_L"

View File

@@ -6617,26 +6617,116 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{F14-300gal}"}, [3] = {["CLSID"]="{F14-300gal}"},
[2] = {["CLSID"]="{SHOULDER AIM_54A_Mk60 L}"}, [2] = {["CLSID"]="{SHOULDER AIM_54A_Mk60 L}"},
[1] = {["CLSID"]="{LAU-138 wtip - AIM-9M}"}}}, [1] = {["CLSID"]="{LAU-138 wtip - AIM-9M}"}}},
["F4U-1D"]={["HVAR x 8"]={[1] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, ["F4U-1D"]={["Drop tank 175 US gal."]={[6] = {["CLSID"]="{175_USgal_Corsair_droptank_aux}"}},
["HVAR x 8"]={[1] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[2] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [2] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[11] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [11] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[10] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [10] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}},
["M-64 bomb x 2, HVAR x 8"]={[1] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[2] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{AN-M64}"},
[7] = {["CLSID"]="{AN-M64}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[10] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[11] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}},
["M-64 bomb x 3, HVAR x 6"]={[2] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{AN-M64}"},
[6] = {["CLSID"]="{AN-M64}"},
[7] = {["CLSID"]="{AN-M64}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[10] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}},
["M-64 bomb x 2, HVAR x 4"]={[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{AN_M65}"},
[7] = {["CLSID"]="{AN_M65}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}}, [9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}},
["Bomb x 2, HVAR x 4"]={[7] = {["CLSID"]="{AN-M64}"}, ["M-64 bomb, M-65 bomb x 2"]={[5] = {["CLSID"]="{AN_M65}"},
[6] = {["CLSID"]="{AN-M64}"},
[7] = {["CLSID"]="{AN_M65}"}},
["Tiny Tim x 2, HVAR x 4"]={[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{Tiny_Tim_Corsair_L}"},
[7] = {["CLSID"]="{Tiny_Tim_Corsair_R}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}},
["Bat Bomb"]={[6] = {["CLSID"]="{ASM_N_2}"}},
["Tiny Tim x 2"]={[5] = {["CLSID"]="{Tiny_Tim_Corsair_L}"},
[7] = {["CLSID"]="{Tiny_Tim_Corsair_R}"}},
["Bat Bomb, HVAR x 8"]={[1] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[2] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [6] = {["CLSID"]="{ASM_N_2}"},
[5] = {["CLSID"]="{AN-M64}"}},
["Tiny Tim x2, HVAR x 4"]={[7] = {["CLSID"]="{Tiny_Tim_Corsair}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[10] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[11] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}}},
["F4U-1D_CW"]={["Drop tank 175 US gal."]={[6] = {["CLSID"]="{175_USgal_Corsair_droptank_aux}"}},
["HVAR x 8"]={[1] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[2] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[11] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[10] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}, [9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{Tiny_Tim_Corsair}"}}}, [8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}},
["M-64 bomb x 2, HVAR x 8"]={[1] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[2] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{AN-M64}"},
[7] = {["CLSID"]="{AN-M64}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[10] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[11] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}},
["M-64 bomb x 3, HVAR x 6"]={[2] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{AN-M64}"},
[6] = {["CLSID"]="{AN-M64}"},
[7] = {["CLSID"]="{AN-M64}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[10] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}},
["M-64 bomb x 2, HVAR x 4"]={[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{AN_M65}"},
[7] = {["CLSID"]="{AN_M65}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}},
["M-64 bomb, M-65 bomb x 2"]={[5] = {["CLSID"]="{AN_M65}"},
[6] = {["CLSID"]="{AN-M64}"},
[7] = {["CLSID"]="{AN_M65}"}},
["Tiny Tim x 2, HVAR x 4"]={[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{Tiny_Tim_Corsair_L}"},
[7] = {["CLSID"]="{Tiny_Tim_Corsair_R}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}},
["Bat Bomb"]={[6] = {["CLSID"]="{ASM_N_2}"}},
["Tiny Tim x 2"]={[5] = {["CLSID"]="{Tiny_Tim_Corsair_L}"},
[7] = {["CLSID"]="{Tiny_Tim_Corsair_R}"}},
["Bat Bomb, HVAR x 8"]={[1] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[2] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[6] = {["CLSID"]="{ASM_N_2}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[10] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[11] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"}}},
["F/A-18A"]={["GBU-16*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3"]={[1] = {["CLSID"]="{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}"}, ["F/A-18A"]={["GBU-16*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3"]={[1] = {["CLSID"]="{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}"},
[2] = {["CLSID"]="{0D33DDAE-524F-4A4E-B5B8-621754FE3ADE}"}, [2] = {["CLSID"]="{0D33DDAE-524F-4A4E-B5B8-621754FE3ADE}"},
[3] = {["CLSID"]="{EFEC8200-B922-11d7-9897-000476191836}"}, [3] = {["CLSID"]="{EFEC8200-B922-11d7-9897-000476191836}"},
@@ -8122,6 +8212,239 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[5] = {["CLSID"]="{R-3S}"}, [5] = {["CLSID"]="{R-3S}"},
[6] = {["CLSID"]="{ASO-2}"}}, [6] = {["CLSID"]="{ASO-2}"}},
["AEROBATIC"]={[7] = {["CLSID"]="{SMOKE_WHITE}"}}}, ["AEROBATIC"]={[7] = {["CLSID"]="{SMOKE_WHITE}"}}},
["MiG-29 Fulcrum"]={["2 * R-27T, 4 * R-73"]={[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[3] = {["CLSID"]="{88DAC840-9F75-4531-8689-B46E64E42E53}"},
[5] = {["CLSID"]="{88DAC840-9F75-4531-8689-B46E64E42E53}"},
[6] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"}},
["2 * R-27R, 4 * R-73"]={[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[3] = {["CLSID"]="{9B25D316-0434-4954-868F-D51DB1A38DF0}"},
[5] = {["CLSID"]="{9B25D316-0434-4954-868F-D51DB1A38DF0}"},
[6] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"}},
["2 * R-27T, 2 * R-73"]={[2] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[3] = {["CLSID"]="{88DAC840-9F75-4531-8689-B46E64E42E53}"},
[5] = {["CLSID"]="{88DAC840-9F75-4531-8689-B46E64E42E53}"},
[6] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"}},
["2 * R-27R, 2 * R-73"]={[2] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[3] = {["CLSID"]="{9B25D316-0434-4954-868F-D51DB1A38DF0}"},
[5] = {["CLSID"]="{9B25D316-0434-4954-868F-D51DB1A38DF0}"},
[6] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"}},
["6 * R-73"]={[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[3] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[5] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"}},
["2 * R-27T, 4 * R-60M"]={[1] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[2] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[3] = {["CLSID"]="{88DAC840-9F75-4531-8689-B46E64E42E53}"},
[5] = {["CLSID"]="{88DAC840-9F75-4531-8689-B46E64E42E53}"},
[6] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[7] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"}},
["2 * R-27R, 4 * R-60M"]={[1] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[2] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[3] = {["CLSID"]="{9B25D316-0434-4954-868F-D51DB1A38DF0}"},
[5] = {["CLSID"]="{9B25D316-0434-4954-868F-D51DB1A38DF0}"},
[6] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[7] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"}},
["2 * R-27T, 2 * R-60M"]={[2] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[3] = {["CLSID"]="{88DAC840-9F75-4531-8689-B46E64E42E53}"},
[5] = {["CLSID"]="{88DAC840-9F75-4531-8689-B46E64E42E53}"},
[6] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"}},
["2 * R-27R, 2 * R-60M"]={[2] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[3] = {["CLSID"]="{9B25D316-0434-4954-868F-D51DB1A38DF0}"},
[5] = {["CLSID"]="{9B25D316-0434-4954-868F-D51DB1A38DF0}"},
[6] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"}},
["6 * R-60"]={[1] = {["CLSID"]="{R_60}"},
[2] = {["CLSID"]="{R_60}"},
[3] = {["CLSID"]="{R_60}"},
[5] = {["CLSID"]="{R_60}"},
[6] = {["CLSID"]="{R_60}"},
[7] = {["CLSID"]="{R_60}"}},
["4 * S-24, 2 * R-73"]={[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[3] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[5] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[6] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"}},
["4 * S-24, 2 * R-60M "]={[1] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[2] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[3] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[5] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[6] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[7] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"}},
["4 * S-24"]={[2] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[3] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[5] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"},
[6] = {["CLSID"]="{3858707D-F5D5-4bbb-BDD8-ABB0530EBC7C}"}},
["80 * S-8OFP"]={[2] = {["CLSID"]="B-8M1 - 20 S-8OFP2"},
[3] = {["CLSID"]="B-8M1 - 20 S-8OFP2"},
[5] = {["CLSID"]="B-8M1 - 20 S-8OFP2"},
[6] = {["CLSID"]="B-8M1 - 20 S-8OFP2"}},
["80 * S-8OFP, 2 * R-60M"]={[1] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[2] = {["CLSID"]="B-8M1 - 20 S-8OFP2"},
[3] = {["CLSID"]="B-8M1 - 20 S-8OFP2"},
[5] = {["CLSID"]="B-8M1 - 20 S-8OFP2"},
[6] = {["CLSID"]="B-8M1 - 20 S-8OFP2"},
[7] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"}},
["80 * S-8KOM, 2 * R-73"]={[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{F72F47E5-C83A-4B85-96ED-D3E46671EE9A}"},
[3] = {["CLSID"]="{F72F47E5-C83A-4B85-96ED-D3E46671EE9A}"},
[5] = {["CLSID"]="{F72F47E5-C83A-4B85-96ED-D3E46671EE9A}"},
[6] = {["CLSID"]="{F72F47E5-C83A-4B85-96ED-D3E46671EE9A}"},
[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"}},
["4 * BetAB-500, 2 * R-60"]={[1] = {["CLSID"]="{R_60}"},
[2] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[3] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[5] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[6] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[7] = {["CLSID"]="{R_60}"}},
["4 * BetAB-500, 2 * R-73"]={[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[3] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[5] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[6] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"}},
["4 * BetAB-500"]={[2] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[3] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[5] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"},
[6] = {["CLSID"]="{35B698AC-9FEF-4EC4-AD29-484A0085F62B}"}},
["4 * KMGU"]={[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[5] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[6] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"}},
["4 * KMGU AO-2.5RT, 2 * R-60M"]={[1] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[5] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[6] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[7] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"}},
["4 * KMGU PTAB-2.5KO, 2 * R-73"]={[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[5] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[6] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"}},
["2 * R-27T"]={[3] = {["CLSID"]="{88DAC840-9F75-4531-8689-B46E64E42E53}"},
[5] = {["CLSID"]="{88DAC840-9F75-4531-8689-B46E64E42E53}"}},
["2 * R-27R"]={[3] = {["CLSID"]="{9B25D316-0434-4954-868F-D51DB1A38DF0}"},
[5] = {["CLSID"]="{9B25D316-0434-4954-868F-D51DB1A38DF0}"}},
["4 * RBK-250 PTAB-2.5M, 2 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{4203753F-8198-4E85-9924-6F8FF679F9FF}"},
[2] = {["CLSID"]="{4203753F-8198-4E85-9924-6F8FF679F9FF}"},
[5] = {["CLSID"]="{4203753F-8198-4E85-9924-6F8FF679F9FF}"},
[3] = {["CLSID"]="{4203753F-8198-4E85-9924-6F8FF679F9FF}"}},
["4 * RBK-250 AO-1SCh, 2 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{RBK_250_275_AO_1SCH}"},
[2] = {["CLSID"]="{RBK_250_275_AO_1SCH}"},
[5] = {["CLSID"]="{RBK_250_275_AO_1SCH}"},
[3] = {["CLSID"]="{RBK_250_275_AO_1SCH}"}},
["4 * RBK-500 PTAB-1M, 2 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{7AEC222D-C523-425e-B714-719C0D1EB14D}"},
[2] = {["CLSID"]="{7AEC222D-C523-425e-B714-719C0D1EB14D}"},
[5] = {["CLSID"]="{7AEC222D-C523-425e-B714-719C0D1EB14D}"},
[3] = {["CLSID"]="{7AEC222D-C523-425e-B714-719C0D1EB14D}"}},
["4 * RBK-500 PTAB-10, 2 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{D5435F26-F120-4FA3-9867-34ACE562EF1B}"},
[2] = {["CLSID"]="{D5435F26-F120-4FA3-9867-34ACE562EF1B}"},
[5] = {["CLSID"]="{D5435F26-F120-4FA3-9867-34ACE562EF1B}"},
[3] = {["CLSID"]="{D5435F26-F120-4FA3-9867-34ACE562EF1B}"}},
["4 * FAB-250M-62, 2 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{FAB_250_M62}"},
[2] = {["CLSID"]="{FAB_250_M62}"},
[5] = {["CLSID"]="{FAB_250_M62}"},
[3] = {["CLSID"]="{FAB_250_M62}"}},
["4 * FAB-500M-62, 2 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[2] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[5] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[3] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"}},
["80 * S-8OFP, 2 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="B-8M1 - 20 S-8OFP2"},
[2] = {["CLSID"]="B-8M1 - 20 S-8OFP2"},
[5] = {["CLSID"]="B-8M1 - 20 S-8OFP2"},
[3] = {["CLSID"]="B-8M1 - 20 S-8OFP2"}},
["4 * BetAB-500ShP, 2 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{BD289E34-DF84-4C5E-9220-4B14C346E79D}"},
[2] = {["CLSID"]="{BD289E34-DF84-4C5E-9220-4B14C346E79D}"},
[5] = {["CLSID"]="{BD289E34-DF84-4C5E-9220-4B14C346E79D}"},
[3] = {["CLSID"]="{BD289E34-DF84-4C5E-9220-4B14C346E79D}"}},
["80 * S-8TsM, 2 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{3DFB7320-AB0E-11d7-9897-000476191836}"},
[2] = {["CLSID"]="{3DFB7320-AB0E-11d7-9897-000476191836}"},
[5] = {["CLSID"]="{3DFB7320-AB0E-11d7-9897-000476191836}"},
[3] = {["CLSID"]="{3DFB7320-AB0E-11d7-9897-000476191836}"}},
["6 * R-60M"]={[7] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[1] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[6] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[2] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[5] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[3] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"}},
["2 * R-27ER, 4 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[5] = {["CLSID"]="{E8069896-8435-4B90-95C0-01A03AE6E400}"},
[3] = {["CLSID"]="{E8069896-8435-4B90-95C0-01A03AE6E400}"}},
["2 * R-27ET, 4 * R-73"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[5] = {["CLSID"]="{B79C379A-9E87-4E50-A1EE-7F7E29C2E87A}"},
[3] = {["CLSID"]="{B79C379A-9E87-4E50-A1EE-7F7E29C2E87A}"}},
["2 * R-27ER, 4 * R-60M"]={[7] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[1] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[6] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[2] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[5] = {["CLSID"]="{E8069896-8435-4B90-95C0-01A03AE6E400}"},
[3] = {["CLSID"]="{E8069896-8435-4B90-95C0-01A03AE6E400}"}},
["2 * R-27ER"]={[5] = {["CLSID"]="{E8069896-8435-4B90-95C0-01A03AE6E400}"},
[3] = {["CLSID"]="{E8069896-8435-4B90-95C0-01A03AE6E400}"}},
["2 * R-27ET"]={[5] = {["CLSID"]="{B79C379A-9E87-4E50-A1EE-7F7E29C2E87A}"},
[3] = {["CLSID"]="{B79C379A-9E87-4E50-A1EE-7F7E29C2E87A}"}},
["2 * R-27ER, 4 * R-73, Fuel"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[5] = {["CLSID"]="{E8069896-8435-4B90-95C0-01A03AE6E400}"},
[3] = {["CLSID"]="{E8069896-8435-4B90-95C0-01A03AE6E400}"},
[4] = {["CLSID"]="{PTB_1500_MIG29A}"}},
["2 * R-27ET, 4 * R-73, fuel"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[5] = {["CLSID"]="{B79C379A-9E87-4E50-A1EE-7F7E29C2E87A}"},
[3] = {["CLSID"]="{B79C379A-9E87-4E50-A1EE-7F7E29C2E87A}"},
[4] = {["CLSID"]="{PTB_1500_MIG29A}"}},
["6 * R-73, Fuel"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[6] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[2] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[5] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[3] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[4] = {["CLSID"]="{PTB_1500_MIG29A}"}},
["6 * R-60M, Fuel"]={[7] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[1] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[6] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[2] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[5] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[3] = {["CLSID"]="{682A481F-0CB5-4693-A382-D00DD4A156D7}"},
[4] = {["CLSID"]="{PTB_1500_MIG29A}"}},
["2 * R-73, Fuel"]={[7] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[1] = {["CLSID"]="{FBC29BFE-3D24-4C64-B81D-941239D12249}"},
[4] = {["CLSID"]="{PTB_1500_MIG29A}"}}},
["Mirage-F1B"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"}, ["Mirage-F1B"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"}, [2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"}, [3] = {["CLSID"]="<CLEAN>"},
@@ -10490,6 +10813,96 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[4] = {["CLSID"]="{MBDA_MistralG}"}, [4] = {["CLSID"]="{MBDA_MistralG}"},
[5] = {["CLSID"]="{FAS}"}, [5] = {["CLSID"]="{FAS}"},
[6] = {["CLSID"]="{IR_Deflector}"}}}, [6] = {["CLSID"]="{IR_Deflector}"}}},
["Mi-28N"]={["2xFAB-250"]={[2] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[3] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"}},
["4xFuel tank"]={[1] = {["CLSID"]="{PTB_450}"},
[2] = {["CLSID"]="{PTB_450}"},
[3] = {["CLSID"]="{PTB_450}"},
[4] = {["CLSID"]="{PTB_450}"}},
["80xS-8"]={[1] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[2] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[3] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[4] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"}},
["4xKMGU AP"]={[1] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[4] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"}},
["4xUPK-23"]={[1] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[2] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[3] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[4] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"}},
["16x9M114, 2xKMGU AT"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["4xFAB-500"]={[1] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[2] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[3] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[4] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"}},
["16x9M114, 2xFAB-500"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[3] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["40xS-8"]={[2] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[3] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"}},
["40xS-8 TsM"]={[2] = {["CLSID"]="B_8V20A_CM"},
[3] = {["CLSID"]="B_8V20A_CM"}},
["2xKMGU AP"]={[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"}},
["2xUPK-23"]={[2] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[3] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"}},
["16x9M114, 2xUPK-23"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[3] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["2xFAB-500"]={[2] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[3] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"}},
["16x9M114, 40xS-8"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[3] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["16x9M114"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["20xS-13"]={[1] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[2] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[3] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[4] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"}},
["16x9M114, 2xKMGU AP"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["4xFAB-250"]={[1] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[2] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[3] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[4] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"}},
["4xKMGU AT"]={[1] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[4] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"}},
["16x9M114, 40xS-8 TsM"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="B_8V20A_CM"},
[3] = {["CLSID"]="B_8V20A_CM"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["80xS-8 TsM"]={[1] = {["CLSID"]="B_8V20A_CM"},
[2] = {["CLSID"]="B_8V20A_CM"},
[3] = {["CLSID"]="B_8V20A_CM"},
[4] = {["CLSID"]="B_8V20A_CM"}},
["2xKMGU AT"]={[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"}},
["9x9M114"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["2xFuel tank"]={[2] = {["CLSID"]="{PTB_450}"},
[3] = {["CLSID"]="{PTB_450}"}},
["10xS-13"]={[2] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[3] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"}},
["2xFAB-250, 16x9M114"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[3] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["16x9M114, 10xS-13"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[3] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}}},
["Tu-95MS"]={["Kh-65*6"]={[1] = {["CLSID"]="{0290F5DE-014A-4BB1-9843-D717749B1DED}"}}},
["B-1B"]={["Mk-82*84"]={[1] = {["CLSID"]="MK_82*28"}, ["B-1B"]={["Mk-82*84"]={[1] = {["CLSID"]="MK_82*28"},
[2] = {["CLSID"]="MK_82*28"}, [2] = {["CLSID"]="MK_82*28"},
[3] = {["CLSID"]="MK_82*28"}}, [3] = {["CLSID"]="MK_82*28"}},
@@ -12278,95 +12691,6 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[5] = {["CLSID"]="{B919B0F4-7C25-455E-9A02-CEA51DB895E3}"}, [5] = {["CLSID"]="{B919B0F4-7C25-455E-9A02-CEA51DB895E3}"},
[6] = {["CLSID"]="{B919B0F4-7C25-455E-9A02-CEA51DB895E3}"}}}, [6] = {["CLSID"]="{B919B0F4-7C25-455E-9A02-CEA51DB895E3}"}}},
["Mi-26"]={}, ["Mi-26"]={},
["Mi-28N"]={["2xFAB-250"]={[2] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[3] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"}},
["4xFuel tank"]={[1] = {["CLSID"]="{PTB_450}"},
[2] = {["CLSID"]="{PTB_450}"},
[3] = {["CLSID"]="{PTB_450}"},
[4] = {["CLSID"]="{PTB_450}"}},
["80xS-8"]={[1] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[2] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[3] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[4] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"}},
["4xKMGU AP"]={[1] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[4] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"}},
["4xUPK-23"]={[1] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[2] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[3] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[4] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"}},
["16x9M114, 2xKMGU AT"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["4xFAB-500"]={[1] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[2] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[3] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[4] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"}},
["16x9M114, 2xFAB-500"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[3] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["40xS-8"]={[2] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[3] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"}},
["40xS-8 TsM"]={[2] = {["CLSID"]="B_8V20A_CM"},
[3] = {["CLSID"]="B_8V20A_CM"}},
["2xKMGU AP"]={[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"}},
["2xUPK-23"]={[2] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[3] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"}},
["16x9M114, 2xUPK-23"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[3] = {["CLSID"]="{05544F1A-C39C-466b-BC37-5BD1D52E57BB}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["2xFAB-500"]={[2] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"},
[3] = {["CLSID"]="{37DCC01E-9E02-432F-B61D-10C166CA2798}"}},
["16x9M114, 40xS-8"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[3] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["16x9M114"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["20xS-13"]={[1] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[2] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[3] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[4] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"}},
["16x9M114, 2xKMGU AP"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74884}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["4xFAB-250"]={[1] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[2] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[3] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[4] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"}},
["4xKMGU AT"]={[1] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[4] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"}},
["16x9M114, 40xS-8 TsM"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="B_8V20A_CM"},
[3] = {["CLSID"]="B_8V20A_CM"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["80xS-8 TsM"]={[1] = {["CLSID"]="B_8V20A_CM"},
[2] = {["CLSID"]="B_8V20A_CM"},
[3] = {["CLSID"]="B_8V20A_CM"},
[4] = {["CLSID"]="B_8V20A_CM"}},
["2xKMGU AT"]={[2] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"},
[3] = {["CLSID"]="{96A7F676-F956-404A-AD04-F33FB2C74881}"}},
["9x9M114"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["2xFuel tank"]={[2] = {["CLSID"]="{PTB_450}"},
[3] = {["CLSID"]="{PTB_450}"}},
["10xS-13"]={[2] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[3] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"}},
["2xFAB-250, 16x9M114"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[3] = {["CLSID"]="{3C612111-C7AD-476E-8A8E-2485812F4E5C}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}},
["16x9M114, 10xS-13"]={[1] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"},
[2] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[3] = {["CLSID"]="{FC56DF80-9B09-44C5-8976-DCFAFF219062}"},
[4] = {["CLSID"]="{57232979-8B0F-4db7-8D9A-55197E06B0F5}"}}},
["Mi-8MT"]={["4 x B8"]={[5] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"}, ["Mi-8MT"]={["4 x B8"]={[5] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[4] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"}, [4] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
[3] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"}, [3] = {["CLSID"]="{6A4B9E69-64FE-439a-9163-3A87FB6A4D81}"},
@@ -15390,7 +15714,6 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[2] = {["CLSID"]="{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}"}, [2] = {["CLSID"]="{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}"},
[1] = {["CLSID"]="{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}"}}, [1] = {["CLSID"]="{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}"}},
["FAB-250*33"]={[3] = {["CLSID"]="{BDAD04AA-4D4A-4E51-B958-180A89F963CF}"}}}, ["FAB-250*33"]={[3] = {["CLSID"]="{BDAD04AA-4D4A-4E51-B958-180A89F963CF}"}}},
["Tu-95MS"]={["Kh-65*6"]={[1] = {["CLSID"]="{0290F5DE-014A-4BB1-9843-D717749B1DED}"}}},
["UH-1H"]={["M134 Minigun*2, XM158*2"]={[1] = {["CLSID"]="M134_L"}, ["UH-1H"]={["M134 Minigun*2, XM158*2"]={[1] = {["CLSID"]="M134_L"},
[2] = {["CLSID"]="XM158_MK5"}, [2] = {["CLSID"]="XM158_MK5"},
[5] = {["CLSID"]="XM158_MK5"}, [5] = {["CLSID"]="XM158_MK5"},

View File

@@ -0,0 +1 @@
AB 250-2 - 144 x SD-2, 250kg CBU with HE submunitions