6 Commits

Author SHA1 Message Date
bobprofisker
a67540c12c Infantry boarding python script
First version of working python script to manage units embarking and disembarking more natively than DCS core can handle / do.
2025-10-10 20:04:15 +01:00
Pax1601
a257afca4b Add customString and customInteger to Unit data model
Introduced customString and customInteger fields to the Unit class in both backend (C++) and frontend (TypeScript/React). Updated data indexes, interfaces, and API handling to support setting and retrieving these custom fields. Also added UI elements in the unit control menu to display and handle these new properties.
2025-09-27 18:07:37 +02:00
Pax1601
3eef91fb24 Add cargo weight and draw argument support
Introduces cargo weight and draw argument properties to units across backend, frontend, and Python API. Adds related commands, data extraction, and registration logic, enabling setting and reading of cargo weight and custom draw arguments for units. Includes new API examples and updates to interfaces, data types, and Lua backend for full feature integration.
2025-09-11 21:47:11 +02:00
Pax1601
73a7ea74f3 feat: Added threshold to unit movement 2025-09-09 18:24:53 +02:00
Pax1601
4e6701ff01 Refactor API callbacks and improve example scripts
Moved register_on_update_callback in api.py for better code organization. Fixed initialization of units_to_delete and corrected simulate_fire_fight usage in example_disembarked_infantry.py. In example_voice_control.py, added cleanup of generated audio files and fixed callback parameter naming for clarity.
2025-08-08 13:14:59 +02:00
Pax1601
5fa1a26843 Add async callbacks and Kronos integration to API
Introduces async callback support for command execution in spawn methods, adds registration/unregistration for update and startup callbacks, and improves logging and signal handling. Adds a new Kronos module and main entry point for initializing and running the API with Kronos integration. Refactors example scripts and updates VSCode launch configurations for new entry points.
2025-08-08 11:06:53 +02:00
42 changed files with 4811 additions and 10108 deletions

View File

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

View File

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

View File

@@ -70,6 +70,10 @@ namespace DataIndex {
aimMethodRange,
acquisitionRange,
airborne,
cargoWeight,
drawArguments,
customString,
customInteger,
lastIndex,
endOfData = 255
};
@@ -159,6 +163,11 @@ namespace DataTypes {
unsigned int ID = 0;
unsigned char detectionMethod = 0;
};
struct DrawArgument {
unsigned int argument = 0;
double value = 0.0;
};
}
#pragma pack(pop)
@@ -167,6 +176,7 @@ bool operator==(const DataTypes::Radio& lhs, const DataTypes::Radio& rhs);
bool operator==(const DataTypes::GeneralSettings& lhs, const DataTypes::GeneralSettings& rhs);
bool operator==(const DataTypes::Ammo& lhs, const DataTypes::Ammo& rhs);
bool operator==(const DataTypes::Contact& lhs, const DataTypes::Contact& rhs);
bool operator==(const DataTypes::DrawArgument& lhs, const DataTypes::DrawArgument& rhs);
struct SpawnOptions {
string unitType;

View File

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

View File

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

View File

@@ -318,4 +318,27 @@ string DeleteSpot::getString()
commandSS << "Olympus.deleteSpot, "
<< spotID;
return commandSS.str();
}
/* SetCargoWeight command */
string SetCargoWeight::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
commandSS << "Olympus.setCargoWeight, "
<< ID << ", "
<< weight;
return commandSS.str();
}
/* RegisterDrawArgument command */
string RegisterDrawArgument::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
commandSS << "Olympus.registerDrawArgument, "
<< ID << ", "
<< argument << ", "
<< active;
return commandSS.str();
}

View File

@@ -12,19 +12,24 @@ bool operator==(const DataTypes::Radio& lhs, const DataTypes::Radio& rhs)
bool operator==(const DataTypes::GeneralSettings& lhs, const DataTypes::GeneralSettings& rhs)
{
return lhs.prohibitAA == rhs.prohibitAA && lhs.prohibitAfterburner == rhs.prohibitAfterburner && lhs.prohibitAG == rhs.prohibitAG &&
return lhs.prohibitAA == rhs.prohibitAA && lhs.prohibitAfterburner == rhs.prohibitAfterburner && lhs.prohibitAG == rhs.prohibitAG &&
lhs.prohibitAirWpn == rhs.prohibitAirWpn && lhs.prohibitJettison == rhs.prohibitJettison;
}
bool operator==(const DataTypes::Ammo& lhs, const DataTypes::Ammo& rhs)
{
return lhs.category == rhs.category && lhs.guidance == rhs.guidance && lhs.missileCategory == rhs.missileCategory &&
return lhs.category == rhs.category && lhs.guidance == rhs.guidance && lhs.missileCategory == rhs.missileCategory &&
lhs.quantity == rhs.quantity && strcmp(lhs.name, rhs.name) == 0;
}
bool operator==(const DataTypes::DrawArgument& lhs, const DataTypes::DrawArgument& rhs)
{
return lhs.argument == rhs.argument && lhs.value == rhs.value;
}
bool operator==(const DataTypes::Contact& lhs, const DataTypes::Contact& rhs)
{
return lhs.detectionMethod == rhs.detectionMethod && lhs.ID == rhs.ID;
return lhs.detectionMethod == rhs.detectionMethod && lhs.ID == rhs.ID;
}

View File

@@ -168,6 +168,12 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
string WP = to_string(i);
double lat = path[i][L"lat"].as_double();
double lng = path[i][L"lng"].as_double();
if (path[i].has_number_field(L"threshold")) {
double threshold = path[i][L"threshold"].as_double();
Coords dest; dest.lat = lat; dest.lng = lng; dest.threshold = threshold;
newPath.push_back(dest);
continue;
}
Coords dest; dest.lat = lat; dest.lng = lng;
newPath.push_back(dest);
}
@@ -183,7 +189,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
string color = to_string(value[L"color"]);
double lat = value[L"location"][L"lat"].as_double();
double lng = value[L"location"][L"lng"].as_double();
Coords loc; loc.lat = lat; loc.lng = lng;
command = dynamic_cast<Command*>(new Smoke(color, loc));
log(username + " added a " + color + " smoke at (" + to_string(lat) + ", " + to_string(lng) + ")", true);
@@ -217,8 +223,8 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
string liveryID = to_string(unit[L"liveryID"]);
string skill = to_string(unit[L"skill"]);
spawnOptions.push_back({unitType, location, loadout, skill, liveryID, heading});
log(username + " spawned a " + coalition + " " + unitType , true);
spawnOptions.push_back({ unitType, location, loadout, skill, liveryID, heading });
log(username + " spawned a " + coalition + " " + unitType, true);
}
if (key.compare("spawnAircrafts") == 0)
@@ -251,8 +257,8 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
Coords location; location.lat = lat; location.lng = lng;
string liveryID = to_string(unit[L"liveryID"]);
string skill = to_string(unit[L"skill"]);
spawnOptions.push_back({ unitType, location, "", skill, liveryID, heading});
spawnOptions.push_back({ unitType, location, "", skill, liveryID, heading });
log(username + " spawned a " + coalition + " " + unitType, true);
}
@@ -398,7 +404,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
unsigned int ID = unit[L"ID"].as_integer();
double lat = unit[L"location"][L"lat"].as_double();
double lng = unit[L"location"][L"lng"].as_double();
Coords location; location.lat = lat; location.lng = lng;
cloneOptions.push_back({ ID, location });
log(username + " cloning unit with ID " + to_string(ID), true);
@@ -427,7 +433,8 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
unsigned char alarmState = value[L"alarmState"].as_number().to_uint32();
unit->setAlarmState(alarmState);
log(username + " set unit " + unit->getUnitName() + "(" + unit->getName() + ") alarm state to " + to_string(alarmState), true);
} else {
}
else {
log("Error while setting setAlarmState. Unit does not exist.");
}
}
@@ -556,7 +563,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
unit->setTargetingRange(value[L"targetingRange"].as_number().to_double());
unit->setAimMethodRange(value[L"aimMethodRange"].as_number().to_double());
unit->setAcquisitionRange(value[L"acquisitionRange"].as_number().to_double());
log(username + " updated unit " + unit->getUnitName() + "(" + unit->getName() + ") engagementProperties", true);
}
}
@@ -581,7 +588,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
Unit* unit = unitsManager->getGroupLeader(ID);
if (unit != nullptr) {
unit->setOnOff(onOff);
log(username + " set unit " + unit->getUnitName() + "(" + unit->getName() + ") onOff to: " + (onOff? "true": "false"), true);
log(username + " set unit " + unit->getUnitName() + "(" + unit->getName() + ") onOff to: " + (onOff ? "true" : "false"), true);
}
}
/************************/
@@ -705,7 +712,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
unitsManager->acquireControl(ID);
unsigned char operateAs = value[L"operateAs"].as_number().to_uint32();
Unit* unit = unitsManager->getGroupLeader(ID);
if (unit != nullptr)
if (unit != nullptr)
unit->setOperateAs(operateAs);
}
/************************/
@@ -811,7 +818,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
command = dynamic_cast<Command*>(new DeleteSpot(spotID));
}
/************************/
else if (key.compare("setCommandModeOptions") == 0)
else if (key.compare("setCommandModeOptions") == 0)
{
setCommandModeOptions(value);
log(username + " updated the Command Mode Options", true);
@@ -821,6 +828,53 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
unitsManager->loadDatabases();
}
/************************/
else if (key.compare("setCargoWeight") == 0)
{
unsigned int ID = value[L"ID"].as_integer();
Unit* unit = unitsManager->getUnit(ID);
if (unit != nullptr) {
double weight = value[L"weight"].as_double();
unit->setCargoWeight(weight);
log(username + " set weight to unit " + unit->getUnitName() + "(" + unit->getName() + "), " + to_string(weight), true);
}
}
/************************/
else if (key.compare("registerDrawArgument") == 0)
{
unsigned int ID = value[L"ID"].as_integer();
Unit* unit = unitsManager->getUnit(ID);
if (unit != nullptr) {
int argument = value[L"argument"].as_integer();
bool active = value[L"active"].as_bool();
command = dynamic_cast<Command*>(new RegisterDrawArgument(ID, argument, active));
log(username + " registered draw argument " + to_string(argument) + " for unit " + unit->getUnitName() + "(" + unit->getName() + "), value:" + to_string(active), true);
}
}
/************************/
else if (key.compare("setCustomString") == 0)
{
unsigned int ID = value[L"ID"].as_integer();
Unit* unit = unitsManager->getUnit(ID);
if (unit != nullptr) {
string customString = to_string(value[L"customString"]);
unit->setCustomString(customString);
log(username + " set custom string to unit " + unit->getUnitName() + "(" + unit->getName() + "), " + customString, true);
}
}
/************************/
else if (key.compare("setCustomInteger") == 0)
{
unsigned int ID = value[L"ID"].as_integer();
Unit* unit = unitsManager->getUnit(ID);
if (unit != nullptr) {
double customNumber = value[L"customInteger"].as_double();
unit->setCustomInteger(customNumber);
log(username + " set custom number to unit " + unit->getUnitName() + "(" + unit->getName() + "), " + to_string(customNumber), true);
}
}
/************************/
else
{
log("Unknown command: " + key);

View File

@@ -128,6 +128,20 @@ void Unit::update(json::value json, double dt)
setAmmo(ammo);
}
if (json.has_object_field(L"drawArguments")) {
vector<DataTypes::DrawArgument> drawArguments;
for (auto const& el : json[L"drawArguments"].as_object()) {
DataTypes::DrawArgument drawArgumentItem;
auto drawArgumentJson = el.second;
if (drawArgumentJson.has_number_field(L"argument"))
drawArgumentItem.argument = drawArgumentJson[L"argument"].as_number().to_uint32();
if (drawArgumentJson.has_number_field(L"value"))
drawArgumentItem.value = drawArgumentJson[L"value"].as_number().to_double();
drawArguments.push_back(drawArgumentItem);
}
setDrawArguments(drawArguments);
}
if (json.has_object_field(L"contacts")) {
vector<DataTypes::Contact> contacts;
for (auto const& el : json[L"contacts"].as_object()) {
@@ -257,8 +271,6 @@ void Unit::getData(stringstream& ss, unsigned long long time)
appendNumeric(ss, datumIndex, alive);
datumIndex = DataIndex::unitID;
appendNumeric(ss, datumIndex, unitID);
datumIndex = DataIndex::groupID;
appendNumeric(ss, datumIndex, groupID);
}
else {
for (unsigned char datumIndex = DataIndex::startOfData + 1; datumIndex < DataIndex::lastIndex; datumIndex++)
@@ -330,6 +342,10 @@ void Unit::getData(stringstream& ss, unsigned long long time)
case DataIndex::aimMethodRange: appendNumeric(ss, datumIndex, aimMethodRange); break;
case DataIndex::acquisitionRange: appendNumeric(ss, datumIndex, acquisitionRange); break;
case DataIndex::airborne: appendNumeric(ss, datumIndex, airborne); break;
case DataIndex::cargoWeight: appendNumeric(ss, datumIndex, cargoWeight); break;
case DataIndex::drawArguments: appendVector(ss, datumIndex, drawArguments); break;
case DataIndex::customString: appendString(ss, datumIndex, customString); break;
case DataIndex::customInteger: appendNumeric(ss, datumIndex, customInteger); break;
}
}
}
@@ -701,6 +717,24 @@ void Unit::setGeneralSettings(DataTypes::GeneralSettings newGeneralSettings, boo
}
}
void Unit::setDrawArguments(vector<DataTypes::DrawArgument> newDrawArguments)
{
if (drawArguments.size() == newDrawArguments.size()) {
bool equal = true;
for (int i = 0; i < drawArguments.size(); i++) {
if (drawArguments.at(i) != newDrawArguments.at(i))
{
equal = false;
break;
}
}
if (equal)
return;
}
drawArguments = newDrawArguments;
triggerUpdate(DataIndex::drawArguments);
}
void Unit::setDesiredSpeed(double newDesiredSpeed)
{
if (desiredSpeed != newDesiredSpeed) {
@@ -767,6 +801,7 @@ void Unit::goToDestination(string enrouteTask)
}
}
// NOTE: if the current active path has a threshold set, that value will be used instead of the passed one
bool Unit::isDestinationReached(double threshold)
{
if (activeDestination != NULL)
@@ -776,7 +811,7 @@ bool Unit::isDestinationReached(double threshold)
{
double dist = 0;
Geodesic::WGS84().Inverse(p->getPosition().lat, p->getPosition().lng, activeDestination.lat, activeDestination.lng, dist);
if (dist < threshold)
if (dist < (activeDestination.threshold == 0? threshold: activeDestination.threshold))
{
log(unitName + " destination reached");
return true;

View File

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

View File

@@ -64,9 +64,9 @@ std::string random_string(size_t length)
return str;
}
bool operator== (const Coords& a, const Coords& b) { return a.lat == b.lat && a.lng == b.lng && a.alt == b.alt; }
bool operator== (const Coords& a, const Coords& b) { return a.lat == b.lat && a.lng == b.lng && a.alt == b.alt && a.threshold == b.threshold; }
bool operator!= (const Coords& a, const Coords& b) { return !(a == b); }
bool operator== (const Coords& a, const double& b) { return a.lat == b && a.lng == b && a.alt == b; }
bool operator== (const Coords& a, const double& b) { return a.lat == b && a.lng == b && a.alt == b && a.threshold == b; }
bool operator!= (const Coords& a, const double& b) { return !(a == b); }
bool operator== (const Offset& a, const Offset& b) { return a.x == b.x && a.y == b.y && a.z == b.z; }

View File

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

View File

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

View File

@@ -219,6 +219,11 @@ export interface Offset {
z: number;
}
export interface DrawingArgument {
argument: number;
value: number;
}
export interface UnitData {
category: string;
markerCategory: string;
@@ -286,6 +291,10 @@ export interface UnitData {
aimMethodRange: number;
acquisitionRange: number;
airborne: boolean;
cargoWeight: number;
drawingArguments: DrawingArgument[];
customString: string;
customInteger: number;
}
export interface LoadoutItemBlueprint {
@@ -314,7 +323,6 @@ export interface UnitBlueprint {
roles?: string[];
type?: string;
loadouts?: LoadoutBlueprint[];
acceptedPayloads?: { [key: string]: { clsids: string[]; names: string[] } };
filename?: string;
liveries?: { [key: string]: { name: string; countries: string[] } };
cost?: number;

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -283,7 +283,7 @@ export function UnitSpawnMenu(props: {
<span
className={`
font-normal
dark:text-gray-400
dark:text-white
`}
>
Altitude
@@ -322,7 +322,7 @@ export function UnitSpawnMenu(props: {
<span
className={`
my-auto font-normal
dark:text-gray-400
dark:text-white
`}
>
Role
@@ -358,7 +358,7 @@ export function UnitSpawnMenu(props: {
<span
className={`
my-auto font-normal
dark:text-gray-400
dark:text-white
`}
>
Weapons
@@ -410,7 +410,7 @@ export function UnitSpawnMenu(props: {
<span
className={`
my-auto font-normal
dark:text-gray-400
dark:text-white
`}
>
Livery
@@ -476,7 +476,7 @@ export function UnitSpawnMenu(props: {
<span
className={`
my-auto font-normal
dark:text-gray-400
dark:text-white
`}
>
Skill
@@ -594,7 +594,7 @@ export function UnitSpawnMenu(props: {
dark:bg-[#17212D]
`}
>
{item.quantity} x
{item.quantity}
</div>
<div
className={`
@@ -756,7 +756,7 @@ export function UnitSpawnMenu(props: {
<span
className={`
font-normal
dark:text-gray-400
dark:text-white
`}
>
Altitude
@@ -795,7 +795,7 @@ export function UnitSpawnMenu(props: {
<span
className={`
my-auto font-normal
dark:text-gray-400
dark:text-white
`}
>
Role
@@ -831,7 +831,7 @@ export function UnitSpawnMenu(props: {
<span
className={`
my-auto font-normal
dark:text-gray-400
dark:text-white
`}
>
Weapons
@@ -875,7 +875,7 @@ export function UnitSpawnMenu(props: {
<span
className={`
my-auto font-normal
dark:text-gray-400
dark:text-white
`}
>
Livery
@@ -944,7 +944,7 @@ export function UnitSpawnMenu(props: {
<span
className={`
my-auto font-normal
dark:text-gray-400
dark:text-white
`}
>
Skill
@@ -1058,7 +1058,7 @@ export function UnitSpawnMenu(props: {
dark:bg-[#17212D]
`}
>
{item.quantity} x
{item.quantity}
</div>
<div
className={`

View File

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

View File

@@ -23,6 +23,7 @@ Olympus.unitIndex = 0 -- Counter used to spread the computational load of data
Olympus.unitStep = 50 -- Max number of units that get updated each cycle
Olympus.units = {} -- Table holding references to all the currently existing units
Olympus.unitsInitialLife = {} -- getLife0 returns 0 for ships, so we need to store the initial life of units
Olympus.drawArguments = {} -- Table that sets what drawArguments to read for each unit
Olympus.weaponIndex = 0 -- Counter used to spread the computational load of data retrievial from DCS
Olympus.weaponStep = 50 -- Max number of weapons that get updated each cycle
@@ -1087,10 +1088,38 @@ function Olympus.setOnOff(groupName, onOff)
end
end
-- Get the unit description
function getUnitDescription(unit)
return unit:getDescr()
end
-- Set the unit cargo weight
function Olympus.setCargoWeight(ID, weight)
Olympus.debug("Olympus.setCargoWeight " .. ID .. " " .. tostring(weight), 2)
local unit = Olympus.getUnitByID(ID)
if unit ~= nil and unit:isExist() then
trigger.action.setUnitInternalCargo(unit:getName(), weight)
end
end
-- Register a drawArgument to be read for a unit
function Olympus.registerDrawArgument(ID, argument, active)
Olympus.debug("Olympus.registerDrawArgument " .. ID .. " " .. tostring(argument) .. " " .. tostring(active), 2)
-- Create the table if it does not exist
if Olympus.drawArguments[ID] == nil then
Olympus.drawArguments[ID] = {}
end
-- Set the draw argument to true or false
if active then
Olympus.drawArguments[ID][argument] = true
else
Olympus.drawArguments[ID][argument] = false
end
end
-- This function gets the navpoints from the DCS mission
function Olympus.getNavPoints()
local function extract_tag(str)
@@ -1293,6 +1322,20 @@ function Olympus.setUnitsData(arg, time)
table["radarState"] = false
end
end ]]
-- Read the draw arguments
local drawArguments = {}
if Olympus.drawArguments[ID] ~= nil then
for argument, active in pairs(Olympus.drawArguments[ID]) do
if active then
drawArguments[#drawArguments + 1] = {
argument = argument,
value = unit:getDrawArgumentValue(argument)
}
end
end
end
table["drawArguments"] = drawArguments
local group = unit:getGroup()
if group ~= nil then

View File

@@ -68,7 +68,7 @@
"args": {
"key": "folder",
"description": "DCS folder location",
"default": "C:\\Program Files\\Eagle Dynamics\\DCS World"
"default": "E:\\Eagle Dynamics\\DCS World (Open Beta)"
}
}
]

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import struct
from typing import List
from data.data_types import LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset
from data.data_types import DrawArgument, LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset
class DataExtractor:
def __init__(self, buffer: bytes):
@@ -48,6 +48,7 @@ class DataExtractor:
lat = self.extract_float64()
lng = self.extract_float64()
alt = self.extract_float64()
threshold = self.extract_float64()
return LatLng(lat, lng, alt)
def extract_from_bitmask(self, bitmask: int, position: int) -> bool:
@@ -136,4 +137,14 @@ class DataExtractor:
x=self.extract_float64(),
y=self.extract_float64(),
z=self.extract_float64()
)
)
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
ACQUISITION_RANGE = 64
AIRBORNE = 65
CARGO_WEIGHT = 66
DRAW_ARGUMENTS = 67
CUSTOM_STRING = 68
CUSTOM_INTEGER = 69
END_OF_DATA = 255

View File

@@ -8,13 +8,15 @@ class LatLng:
lat: float
lng: float
alt: float
threshold: Optional[float] = 0 # Optional threshold for proximity checks
def toJSON(self):
"""Convert LatLng to a JSON serializable dictionary."""
return {
"lat": self.lat,
"lng": self.lng,
"alt": self.alt
"alt": self.alt,
"threshold": self.threshold
}
def project_with_bearing_and_distance(self, d, bearing):
@@ -88,4 +90,9 @@ class Contact:
class Offset:
x: float
y: float
z: float
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
import logging
logger = logging.getLogger("TestBed")
logger = logging.getLogger("example_disembarked_infantry")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
units_to_delete = None
units_to_delete = []
#############################################################################################
# This class represents a disembarked infantry unit that will engage in combat
@@ -28,7 +28,7 @@ class DisembarkedInfantry(Unit):
with the closest enemy unit.
"""
logger.info(f"Unit {self.unit_id} is now fighting.")
# Pick a random target
target = self.pick_random_target()
@@ -86,7 +86,7 @@ class DisembarkedInfantry(Unit):
# Simulate a firefight in the direction of the enemy
firefight_destination = self.position.project_with_bearing_and_distance(30, bearing_to_enemy)
self.simulate_fire_fight(firefight_destination.lat, firefight_destination.lng, firefight_destination.alt + 1)
self.simulate_fire_fight(firefight_destination, firefight_destination.alt + 1)
await asyncio.sleep(10) # Simulate some time spent in firefight
self.start_fighting() # Restart the fighting process
@@ -109,8 +109,8 @@ def on_api_startup(api: API):
if unit.alive and not unit.human and unit.coalition == "blue":
units_to_delete.append(unit)
try:
unit.delete_unit(False, "", True)
unit.register_on_property_change_callback("alive", on_unit_alive_change)
unit.delete_unit(False, "", True)
logger.info(f"Deleted unit: {unit}")
except Exception as e:
@@ -175,7 +175,7 @@ def on_api_update(api: API):
new_unit.__class__ = DisembarkedInfantry
new_unit.start_fighting()
api.spawn_ground_units([spawn_table], unit.coalition, "", True, 0, lambda new_group_ID: execution_callback(new_group_ID))
api.spawn_ground_units([spawn_table], unit.coalition, "", True, 0, execution_callback)
logger.info(f"Spawned new unit succesfully at {spawn_position} with heading {unit.heading}")
break

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

View File

@@ -0,0 +1,776 @@
import asyncio
from asyncio import Semaphore
from random import randrange
from api import API, Unit, UnitSpawnTable
from math import pi
import logging
import time
#Set some globals up
before_can_re_embark_time = 300 # this is the time it takes for the infantry, after disembarking, to become embarkable again
min_toggle_time_period = 30 # this should typically be however long it takes the longest thing to load or unload, used to prevent accidental re toggling the toggle switch too early by accident
####Transport types#####
transport_ground = {}
transport_helicopters = {
"UH-1H":{
"max_capacity": 8,
"max_embark_range": 100,
"doors": 2,
"door_positions": [(2.5,-pi/2),(0.8,0),(2.5,pi/2),(0.8,0)], #two values here offset and heading offset in radians and second distance offset and heading offset in radians
"centre_offset_position": [(0,0),(0,0)], #used for calculating the unit door centre, when the doors aren't in line with the centre
"door_argument_nos": [43,44], #draw argument numbers for the doors
"door_open_thresholds": [0.8,0.8], #value above which the door is considered open
"is_rear_loader": False,
"boarding_distance": 5,
"rotor_radius": 15
},
"CH-47Fbl1":{
"max_capacity": 30,
"max_embark_range": 100,
"doors": 1,
"door_positions": [(12,-pi),(0,0)], #two values here offset and heading offset in radians and second distance offset and heading offset in radians
"centre_offset_position": [(11,-pi),(0,0)], #used for calculating the unit door centre, when the doors aren't in line with the centre
"door_argument_nos": [86], #draw argument numbers for the doors
"door_open_thresholds": [0.55], #value above which the door is considered open
"is_rear_loader": True,
"boarding_distance": 10,
"rotor_radius": 31
},
"Mi-8MT":{
"max_capacity": 24,
"max_embark_range": 100,
"doors": 1,
"door_positions": [(6,-pi),(0,0)], #two values here offset and heading offset in radians and second distance offset and heading offset in radians
"centre_offset_position": [(5.5,-pi),(0,0)], #used for calculating the unit door centre, when the doors aren't in line with the centre
"door_argument_nos": [86], #draw argument numbers for the doors
"door_open_thresholds": [0.8], #value above which the door is considered open
"is_rear_loader": True,
"boarding_distance": 10,
"rotor_radius": 22
},
"Mi-24P":{
"max_capacity": 8,
"max_embark_range": 100,
"doors": 2,
"door_positions": [(2.5,+pi/2),(1.2,0),(2.5,-pi/2),(1.2,0)], #two values here offset and heading offset in radians and second distance offset and heading offset in radians
"centre_offset_position": [(0,0),(0,0)], #used for calculating the unit door centre, when the doors aren't in line with the centre
"door_argument_nos": [38,86], #draw argument numbers for the doors
"door_open_thresholds": [0.8,0.8], #value above which the door is considered open
"is_rear_loader": False,
"boarding_distance": 5,
"rotor_radius": 18
}
}
transport_types = set(transport_helicopters.keys()).union(transport_ground.keys())
#Infantry transport
embarker_inf_red = {}
embarker_inf_blue = {"Soldier M4 GRG","soldier_wwii_us"}
embarker_types = embarker_inf_blue.union(embarker_inf_red)
#Time it takes after loading or unloading to swap back to the other
# Setup a logger for the module
logger = logging.getLogger("infantry_boarding")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
def cross_product_2d(v1, v2):
return v1[0] * v2[1] - v1[1] * v2[0]
def determine_side(self, door):
# Calculate relative vectors
vector_to_unit = [
self.position.lat - self.transport_unit.position.lat,
self.position.lng - self.transport_unit.position.lng
]
vector_to_door = [
door.lat - self.transport_unit.position.lat,
door.lng - self.transport_unit.position.lng
]
# Compute the 2D cross product
cross_z = cross_product_2d(vector_to_unit, vector_to_door)
# Determine the side based on the sign of the cross product
if cross_z > 0:
return True
elif cross_z < 0:
return False
else:
return True
class Transporter(Unit):
def __init__(self, Unit):
self.unit = Unit
def set_as_transport(self):
self.unit.is_transport = True
if self.unit.name in transport_helicopters:
if self.unit.name == "UH-1H":
self.unit.max_capacity = transport_helicopters["UH-1H"]["max_capacity"]
self.unit.max_embark_range = transport_helicopters["UH-1H"]["max_embark_range"]
self.unit.boarding_distance = transport_helicopters["UH-1H"]["boarding_distance"]
self.unit.current_capacity = 0
self.unit.current_cargo_weight = 0
self.unit.unit_array = []
self.unit.en_boarding_queue = []
self.unit.current_max_capacity = 0
self.unit.doors = transport_helicopters["UH-1H"]["doors"]
self.unit.door_positions = transport_helicopters["UH-1H"]["door_positions"]
self.unit.centre_offset_position = transport_helicopters["UH-1H"]["centre_offset_position"]
self.unit.door_argument_nos = transport_helicopters["UH-1H"]["door_argument_nos"]
self.unit.door_open_thresholds = transport_helicopters["UH-1H"]["door_open_thresholds"]
self.unit.will_disembark = True
self.unit.register_draw_argument(43) #left door
self.unit.register_draw_argument(44) #right door
self.unit.register_draw_argument(446) #interior light colour switch, we use as a toggle
self.unit.loading_toggle_argument = 2 #this is the argument registered in the index on the draw arguments that controls loading state, have to trial and error what it is for each transport type
self.unit.disembark_embark_argument_toggle_argument_threshold = 0.7
self.unit.is_rear_loader = transport_helicopters["UH-1H"]["is_rear_loader"]
self.unit.rotor_radius = transport_helicopters["UH-1H"]["rotor_radius"]
elif self.unit.name == "CH-47Fbl1":
self.unit.max_capacity = transport_helicopters["CH-47Fbl1"]["max_capacity"]
self.unit.max_embark_range = transport_helicopters["CH-47Fbl1"]["max_embark_range"]
self.unit.boarding_distance = transport_helicopters["CH-47Fbl1"]["boarding_distance"]
self.unit.current_capacity = 0
self.unit.current_cargo_weight = 0
self.unit.unit_array = []
self.unit.en_boarding_queue = []
self.unit.current_max_capacity = 0
self.unit.doors = transport_helicopters["CH-47Fbl1"]["doors"]
self.unit.centre_offset_position = transport_helicopters["CH-47Fbl1"]["centre_offset_position"]
self.unit.door_positions = transport_helicopters["CH-47Fbl1"]["door_positions"]
self.unit.door_argument_nos = transport_helicopters["CH-47Fbl1"]["door_argument_nos"]
self.unit.door_open_thresholds = transport_helicopters["CH-47Fbl1"]["door_open_thresholds"]
self.unit.will_disembark = True
self.unit.register_draw_argument(86) #rear ramp
self.unit.register_draw_argument(606) #interior light colour switch, we use as a toggle
self.unit.loading_toggle_argument = 1 #this is the argument registered in the index on the draw arguments that controls loading state, have to trial and error what it is for each transport type
self.unit.disembark_embark_argument_toggle_argument_threshold = 0.5
self.unit.is_rear_loader = transport_helicopters["CH-47Fbl1"]["is_rear_loader"]
self.unit.rotor_radius = transport_helicopters["CH-47Fbl1"]["rotor_radius"]
elif self.unit.name == "Mi-8MT":
self.unit.max_capacity = transport_helicopters["Mi-8MT"]["max_capacity"]
self.unit.max_embark_range = transport_helicopters["Mi-8MT"]["max_embark_range"]
self.unit.boarding_distance = transport_helicopters["Mi-8MT"]["boarding_distance"]
self.unit.current_capacity = 0
self.unit.current_cargo_weight = 0
self.unit.unit_array = []
self.unit.en_boarding_queue = []
self.unit.current_max_capacity = 0
self.unit.doors = transport_helicopters["Mi-8MT"]["doors"]
self.unit.centre_offset_position = transport_helicopters["Mi-8MT"]["centre_offset_position"]
self.unit.door_positions = transport_helicopters["Mi-8MT"]["door_positions"]
self.unit.door_argument_nos = transport_helicopters["Mi-8MT"]["door_argument_nos"]
self.unit.door_open_thresholds = transport_helicopters["Mi-8MT"]["door_open_thresholds"]
self.unit.will_disembark = True
self.unit.register_draw_argument(86) #rear ramp
self.unit.register_draw_argument(133) #interior light colour switch, we use as a toggle
self.unit.loading_toggle_argument = 1 #this is the argument registered in the index on the draw arguments that controls loading state, have to trial and error what it is for each transport type
self.unit.disembark_embark_argument_toggle_argument_threshold = 0.8
self.unit.is_rear_loader = transport_helicopters["Mi-8MT"]["is_rear_loader"]
self.unit.rotor_radius = transport_helicopters["Mi-8MT"]["rotor_radius"]
elif self.unit.name == "Mi-24P":
self.unit.max_capacity = transport_helicopters["Mi-24P"]["max_capacity"]
self.unit.max_embark_range = transport_helicopters["Mi-24P"]["max_embark_range"]
self.unit.boarding_distance = transport_helicopters["Mi-24P"]["boarding_distance"]
self.unit.current_capacity = 0
self.unit.current_cargo_weight = 0
self.unit.unit_array = []
self.unit.en_boarding_queue = []
self.unit.current_max_capacity = 0
self.unit.doors = transport_helicopters["Mi-24P"]["doors"]
self.unit.centre_offset_position = transport_helicopters["Mi-24P"]["centre_offset_position"]
self.unit.door_positions = transport_helicopters["Mi-24P"]["door_positions"]
self.unit.door_argument_nos = transport_helicopters["Mi-24P"]["door_argument_nos"]
self.unit.door_open_thresholds = transport_helicopters["Mi-24P"]["door_open_thresholds"]
self.unit.will_disembark = True
self.unit.register_draw_argument(38) #left door
self.unit.register_draw_argument(86) #right door
self.unit.register_draw_argument(47) #interior light colour switch, we use as a toggle
self.unit.loading_toggle_argument = 2 #this is the argument registered in the index on the draw arguments that controls loading state, have to trial and error what it is for each transport type
self.unit.disembark_embark_argument_toggle_argument_threshold = 0.8
self.unit.is_rear_loader = transport_helicopters["Mi-24P"]["is_rear_loader"]
self.unit.rotor_radius = transport_helicopters["Mi-24P"]["rotor_radius"]
else:
pass
class DisembarkedInfantry(Unit):
def __str__(self):
return f"DisembarkedInfrantry(unit_id={self.unit_id}, group_id={self.group_id}, position={self.position}, heading={self.heading})"
def __init__(self, Unit):
self.unit = Unit
def disembark_from_transport(self):
destination = self.position.project_with_bearing_and_distance(30, self.heading)
# Set the destination for the unit
self.set_roe(4) #set to hold fire to avoid stopping to shoot
self.is_loadable = False
self.set_path([destination])
if self.check_for_enemy_in_range():
self.set_speed(10)
else:
self.set_speed(2)
self.register_on_destination_reached_callback(
self.on_destination_reached,
destination,
threshold=15.0,
timeout=30.0 # Timeout after 30 seconds if the destination is not reached
)
def check_for_enemy_in_range(self):
units = api.get_units()
for unit in units.values():
if unit.alive and unit.coalition != self.coalition:
distance_to_enemy = self.position.distance_to(unit.position)
if distance_to_enemy < 2000: #if an enemy is within 2000m, approx rifle max range
return True
return False
async def on_destination_reached(self, _, reached: bool):
if not reached:
# logger.info(f"Unit {self} did not reach its destination.")
self.set_roe(1)
new_patrol = self.position.project_with_bearing_and_distance(1000, self.transport_spawn_heading)
await asyncio.sleep(10) #wait a bit before trying again
try:
if self.og_transport.is_rear_loader:
side_offset = self.position.project_with_bearing_and_distance(30,self.transport_spawn_heading-pi/2)
self.set_path([side_offset,new_patrol])
else:
self.set_path([new_patrol])
except AttributeError:
self.set_path([new_patrol])
if self.check_for_enemy_in_range():
self.set_speed(10)
else:
self.set_speed(1.3)
await asyncio.sleep(before_can_re_embark_time) #wait before setting to be boardable
self.is_loadable = True
logger.info(f"Unit {self} is now boardable again.")
else:
self.set_roe(1)
new_patrol = self.position.project_with_bearing_and_distance(1000, self.transport_spawn_heading)
await asyncio.sleep(10) #wait a bit before trying again
try:
if self.og_transport.is_rear_loader:
side_offset = self.position.project_with_bearing_and_distance(30,self.transport_spawn_heading-pi/2)
self.set_path([side_offset,new_patrol])
else:
self.set_path([new_patrol])
except AttributeError:
self.set_path([new_patrol])
if self.check_for_enemy_in_range():
self.set_speed(10)
else:
self.set_speed(1.3)
await asyncio.sleep(before_can_re_embark_time) #wait before setting to be boardable
self.is_loadable = True
logger.info(f"Unit {self} is now boardable again.")
class Embarker(Unit):
def __str__(self):
return f"DisembarkedInfrantry(unit_id={self.unit_id}, group_id={self.group_id}, position={self.position}, heading={self.heading})"
def __init__(self, Unit):
self.unit = Unit
def set_as_embarker(self):
self.unit.is_embarker = True
self.unit.is_moving = False
self.unit.is_loadable = True
logger.info(f"Set unit '{self.unit.name}' as embarker.")
def can_board(self):
transport = self.transport_unit
if transport.current_capacity < transport.max_capacity:
transport.unit_array.append(self.name)
transport.current_capacity += 1
self.delete_unit()
else:
pass
def board_transport(self):
door, num_doors_open = self.get_closest_door()
if num_doors_open > 1:
door_bypass = True
else:
door_bypass = False
if door is None:
pass
elif door is not None:
if self.is_moving:
pass
elif not self.is_moving:
distance = self.position.distance_to(door)
distance_centre_offset_position = self.position.distance_to(self.transport_unit.position.project_with_bearing_and_distance(self.transport_unit.centre_offset_position[0][0], self.transport_unit.centre_offset_position[0][1]))
if distance >= distance_centre_offset_position:
if determine_side(self,door): #right side
if self.transport_unit.is_rear_loader: # chinook rear loader
destination = door.project_with_bearing_and_distance(self.transport_unit.rotor_radius/3, self.transport_unit.heading-pi)
destination = destination.project_with_bearing_and_distance(self.transport_unit.rotor_radius/2, self.transport_unit.heading+pi/2)
destination.threshold = 2
self.set_path([destination])
self.register_on_destination_reached_callback(
self.on_destination_reached,
destination,
threshold=2.0,
timeout=10.0 # Timeout after 30 seconds if the destination is not reached
)
self.is_moving = True
else: # huey front loader
destination = door.project_with_bearing_and_distance(self.transport_unit.rotor_radius, self.transport_unit.heading)
destination = destination.project_with_bearing_and_distance(self.transport_unit.rotor_radius/2, self.transport_unit.heading+pi/2)
destination.threshold = 2
self.set_path([destination])
self.register_on_destination_reached_callback(
self.on_destination_reached,
destination,
threshold=2.0,
timeout=10.0 # Timeout after 30 seconds if the destination is not reached
)
self.is_moving = True
else: #left side
if self.transport_unit.is_rear_loader: # chinook rear loader
destination = door.project_with_bearing_and_distance(self.transport_unit.rotor_radius/3, self.transport_unit.heading-pi)
destination = destination.project_with_bearing_and_distance(self.transport_unit.rotor_radius/2, self.transport_unit.heading-pi/2)
destination.threshold = 2
self.set_path([destination])
self.register_on_destination_reached_callback(
self.on_destination_reached,
destination,
threshold=2.0,
timeout=10.0 # Timeout after 30 seconds if the destination is not reached
)
self.is_moving = True
else: # huey front loader
destination = door.project_with_bearing_and_distance(self.transport_unit.rotor_radius, self.transport_unit.heading)
destination = destination.project_with_bearing_and_distance(self.transport_unit.rotor_radius/2, self.transport_unit.heading-pi/2)
destination.threshold = 2
self.set_path([destination])
self.register_on_destination_reached_callback(
self.on_destination_reached,
destination,
threshold=2.0,
timeout=10.0 # Timeout after 30 seconds if the destination is not reached
)
self.is_moving = True
else:
destination = self.position.project_with_bearing_and_distance(distance+2, self.position.bearing_to(door))
#destination = destination.project_with_bearing_and_distance(self.transport_unit.rotor_radius, self.transport_unit.heading+pi/2)
destination.threshold = 2
self.set_path([destination,door])
self.is_moving = True
def get_closest_door(self):
return check_closest_open_door(self.transport_unit, self)
async def on_destination_reached(self, _, reached: bool):
if not reached:
logger.info(f"Unit {self} did not reach its destination.")
self.is_moving = False
else:
logger.info(f"Unit {self} has reached its destination.")
self.is_moving = False
await asyncio.sleep(10)
self.board_transport() # Attempt to board again
def check_closest_open_door(transport, embarker):
if transport.name in transport_helicopters:
if transport.door_argument_nos is None and transport.doors > 0:
return transport.position.project_with_bearing_and_distance(5,transport.heading + pi), transport.heading + pi
elif transport.door_argument_nos is not None and transport.doors > 0:
closest_door = None
doors_open = 0
distance_to_closest_door = float('inf')
for i in range(transport.doors):
if transport.draw_arguments[i].value >= transport.door_open_thresholds[i]:
doors_open += 1
distance = embarker.position.distance_to(transport.position.project_with_bearing_and_distance(transport.door_positions[i*2][0], transport.heading + transport.door_positions[i*2][1]).project_with_bearing_and_distance(transport.door_positions[i*2+1][0], transport.heading + transport.door_positions[i*2+1][1]))
if distance < distance_to_closest_door:
distance_to_closest_door = distance
closest_door = transport.position.project_with_bearing_and_distance(transport.door_positions[i*2][0], transport.heading + transport.door_positions[i*2][1]).project_with_bearing_and_distance(transport.door_positions[i*2+1][0], transport.heading + transport.door_positions[i*2+1][1])
return closest_door, doors_open
else:
return None, 0
def check_for_door_status(transporter):
if not hasattr(transporter, 'draw_arguments') or len(transporter.draw_arguments) < transporter.doors:
#logger.warning(f"Transporter '{transporter.name}' does not have enough draw arguments registered.")
return False
if transporter.name in transport_helicopters:
if transporter.door_argument_nos is None and transporter.doors > 0:
return True
elif transporter.door_argument_nos is not None and transporter.doors > 0:
a_door_is_open = False
for i in range(transporter.doors):
if i >= len(transporter.draw_arguments): # Ensure the index is valid
#logger.error(f"Index {i} out of range for draw_arguments in transporter '{transporter.name}'.")
continue
if transporter.draw_arguments[i].value >= transporter.door_open_thresholds[i]:
a_door_is_open = True
return a_door_is_open
else:
return False
elif transporter.name in transport_ground:
if transporter.door_argument_nos is None and transporter.doors > 0:
return True
elif transporter.door_argument_nos is not None and transporter.doors > 0:
a_door_is_open = False
for i in range(transporter.doors):
if i >= len(transporter.draw_arguments): # Ensure the index is valid
#logger.error(f"Index {i} out of range for draw_arguments in transporter '{transporter.name}'.")
continue
if transporter.draw_arguments[i].value >= transporter.door_open_thresholds[i]:
a_door_is_open = True
return a_door_is_open
else:
return False
async def load_loadable_units():
units = api.get_units()
for embarker in units.values():
if embarker.alive and hasattr(embarker, 'is_embarker'):
if hasattr(embarker, 'in_embark_queue') and hasattr(embarker, 'transport_unit') and hasattr(embarker, 'is_moving'):
if embarker.transport_unit.name in transport_types:
if embarker.roe != "hold":
embarker.set_roe(4) #set to hold fire to avoid stopping to shoot
#check the doors are open
if check_for_door_status(embarker.transport_unit):
closest_door, num_doors_open = check_closest_open_door(embarker.transport_unit, embarker)
if closest_door is not None:
#print(f"A door is open on {embarker.transport_unit.name}, closest door is {closest_door}, {num_doors_open} doors open")
embarker.__class__ = Embarker
#check if close enough to board
closest_door, _ = embarker.get_closest_door()
door_distance = embarker.position.distance_to(closest_door)
if door_distance < embarker.transport_unit.boarding_distance or embarker.position.distance_to(embarker.transport_unit.position) < embarker.transport_unit.boarding_distance:
transport = embarker.transport_unit
embarker_units = [
(embarker, embarker.position.distance_to(transport.position))
for embarker in units.values()
if embarker.alive
and hasattr(embarker, 'is_embarker')
and embarker.position.distance_to(closest_door) < transport.boarding_distance
]
embarkers_sorted = sorted(embarker_units, key=lambda x: x[1])
if not embarkers_sorted:
pass
else:
if embarker.ID == embarkers_sorted[0][0].ID:
transport.current_capacity += 1
transport.current_max_capacity +=1
transport.unit_array.append(embarker)
transport.set_cargo_weight(transport.current_cargo_weight + 100) #assume 100kg per infantry with kit
transport.current_cargo_weight += 100
embarker.delete_unit()
#asyncio.create_task(set_as_disembarking(transport))
break
#else run it closer
if embarker.is_moving:
if hasattr(embarker, 'last_pos'):
if embarker.position == embarker.last_pos:
embarker.is_moving = False
embarker.last_pos = embarker.position
pass
elif not embarker.is_moving:
embarker.board_transport()
else:
#no doors so do nothing
pass
def generate_transport_units():
units = api.get_units()
for unit in units.values():
if unit.alive and unit.name in transport_types and not hasattr(unit, 'is_transport'):
new_transport = Transporter(unit)
new_transport.set_as_transport()
elif unit.alive and unit.name in embarker_types and not hasattr(unit, 'is_embarker'):
new_emabarquee = Embarker(unit)
new_emabarquee.set_as_embarker()
async def set_as_disembarking(transport):
transport.will_disembark = True
transport.en_boarding_queue = []
async def set_as_not_disembarking(transport):
transport.will_disembark = False
transport.current_max_capacity = transport.current_capacity
unload_semaphore = Semaphore(1)
async def check_for_unloadable_units():
# Use the semaphore to ensure only one instance runs at a time
async with unload_semaphore:
units = api.get_units()
try:
for transporter in units.values():
if transporter.alive and hasattr(transporter, 'is_transport') and transporter.will_disembark:
# Check if the transporter is in a position to disembark units
if transporter.speed < 2 and check_for_door_status(transporter) and not transporter.airborne and transporter.current_capacity > 0: # check speed is less than 2 m/s and doors are open
# Transport is ready to disembark
to_remove = [] # Sets up variable to hold units to remove from queue
for disembarker in transporter.unit_array:
# Get the open doors
open_doors = []
open_doors_headings = []
for i in range(transporter.doors):
if transporter.draw_arguments[i].value >= transporter.door_open_thresholds[i]:
door_position = transporter.position.project_with_bearing_and_distance(
transporter.door_positions[i * 2][0],
transporter.heading + transporter.door_positions[i * 2][1]
).project_with_bearing_and_distance(
transporter.door_positions[i * 2 + 1][0],
transporter.heading + transporter.door_positions[i * 2 + 1][1]
)
door_heading = transporter.heading + transporter.door_positions[i * 2][1]
open_doors.append(door_position)
open_doors_headings.append(door_heading)
# Round-robin spawn mechanism
if not hasattr(transporter, 'last_door_index'):
transporter.last_door_index = 0 # Initialize the last used door index
# Get the next door in the round-robin sequence
async def execution_callback(new_group_ID: int):
logger.info(f"New units spawned, groupID: {new_group_ID}")
units = api.get_units()
for new_unit in units.values():
if new_unit.group_id == new_group_ID:
logger.info(f"New unit spawned: {new_unit}")
new_unit.__class__ = DisembarkedInfantry
new_unit.transport_spawn_heading = transporter.heading
new_unit.og_transport = transporter
new_unit.disembark_from_transport()
new_unit.original_position = new_unit.position
# The delay is a function of how many units are left to disembark and how long it takes to get to the disembark spot
async def delayed_spawn(delay,transporter,open_doors,open_doors_headings,disembarker):
door_index = transporter.last_door_index % len(open_doors)
transporter.last_door_index += 1
# Increment the door index for the next spawn
# Spawn the unit at the selected door
door_position = open_doors[door_index]
door_heading = open_doors_headings[door_index]
spawn_table: UnitSpawnTable = UnitSpawnTable(
unit_type=disembarker.name,
location=door_position,
heading=door_heading,
skill="High",
livery_id=""
)
# Add a delay before spawning the unit
await asyncio.sleep(delay) # Delay of 2 seconds (adjust as needed)
api.spawn_ground_units([spawn_table], transporter.coalition, "", True, 0, execution_callback)
transporter.set_cargo_weight(transporter.current_cargo_weight - 100) # Assume 100kg per infantry with kit
transporter.current_cargo_weight -= 100
logger.info(f"Spawned unit '{disembarker.name}' from open door of transport '{transporter.name}'.")
if len(open_doors) > 1:
if (transporter.current_max_capacity - transporter.current_capacity) < len(open_doors):
delay = 0.1
else:
delay = (transporter.current_max_capacity - transporter.current_capacity) * 1.25 - ((len(open_doors)-1) * 2.5) + 2.5
else:
delay = (transporter.current_max_capacity - transporter.current_capacity) * 2.5
asyncio.create_task(delayed_spawn(delay,transporter,open_doors,open_doors_headings,disembarker))
transporter.en_boarding_queue = []
transporter.current_capacity -= 1
to_remove.append(disembarker)
for disembarker in to_remove:
transporter.unit_array.remove(disembarker)
except Exception as e:
#logging.warning(e, exc_info=True)
logger.info(f"Error in check_for_unloadable_units: {e}")
async def check_for_loadable_units():
units = api.get_units()
try:
for transporter in units.values():
if transporter.alive and hasattr(transporter, 'is_transport') and not transporter.will_disembark:
if len(transporter.unit_array) < transporter.max_capacity:
if transporter.speed < 2 and check_for_door_status(transporter): #check speed is less than 2 m/s and doors are open
# print("Speed is okay")
embarker_units = [
(embarker, embarker.position.distance_to(transporter.position))
for embarker in units.values()
if embarker.alive
and hasattr(embarker, 'is_embarker')
and getattr(embarker, 'is_loadable', True) # Check if is_loadable is True
and embarker.position.distance_to(transporter.position) < transporter.max_embark_range
]
if embarker_units is None or len(embarker_units) == 0:
continue
else:
for embarker in embarker_units:
if hasattr(embarker, 'in_embark_queue') and embarker.in_embark_queue:
if embarker.in_embark_queue:
embarker_units.remove(embarker)
embarkers_sorted = sorted(embarker_units, key=lambda x: x[1])
closest_embarkers = embarkers_sorted[:transporter.max_capacity-len(transporter.en_boarding_queue)]
for embarker, distance in closest_embarkers:
if embarker not in transporter.en_boarding_queue and distance < transporter.max_embark_range and not hasattr(embarker, 'is_in_queue'):
transporter.en_boarding_queue.append(embarker)
embarker.in_embark_queue = True
embarker.is_in_queue = True
embarker.transport_unit = transporter
logger.info(f"Added embarker '{embarker.name}' to '{transporter.name}' s boarding queue.")
elif embarker not in transporter.en_boarding_queue and distance < transporter.max_embark_range and hasattr(embarker, 'is_in_queue'):
if embarker.is_in_queue:
await asyncio.sleep(60) #wait a bit and try again next time
embarker.is_in_queue = False
else:
transporter.en_boarding_queue.append(embarker)
embarker.in_embark_queue = True
embarker.is_in_queue = True
embarker.transport_unit = transporter
logger.info(f"Added embarker '{embarker.name}' to '{transporter.name}' s boarding queue.")
elif embarker in transporter.en_boarding_queue:
pass
else:
pass #we pass as the transport is full
except Exception as e:
logger.error(f"Error in check_for_loadable_units: {e}")
async def check_for_transport_embarker_or_disembark():
units = api.get_units()
try:
for transporter in units.values():
if transporter.alive and hasattr(transporter, 'is_transport'):
# Ensure the transporter has a `last_toggle_time` attribute
if not hasattr(transporter, 'last_toggle_time'):
transporter.last_toggle_time = 0 # Initialize it to 0
# Get the current time
current_time = time.time()
# Check if the toggle is allowed (min_toggle_time_period seconds since the last toggle)
if current_time - transporter.last_toggle_time < min_toggle_time_period:
continue # Skip toggling if the cooldown hasn't passed
# Check the loading toggle argument and toggle the state
if transporter.loading_toggle_argument is None or not hasattr(transporter, 'draw_arguments') or len(transporter.draw_arguments) <= transporter.loading_toggle_argument:
pass
else:
if transporter.will_disembark:
if transporter.draw_arguments[transporter.loading_toggle_argument].value <= transporter.disembark_embark_argument_toggle_argument_threshold:
continue
elif transporter.draw_arguments[transporter.loading_toggle_argument].value > transporter.disembark_embark_argument_toggle_argument_threshold:
# Set to embark
await set_as_not_disembarking(transporter)
transporter.last_toggle_time = current_time # Update the last toggle time
logger.info(f"Transporter '{transporter.name}' set to embark.")
else:
if transporter.draw_arguments[transporter.loading_toggle_argument].value <= transporter.disembark_embark_argument_toggle_argument_threshold:
# Set to disembark
await set_as_disembarking(transporter)
transporter.last_toggle_time = current_time # Update the last toggle time
logger.info(f"Transporter '{transporter.name}' set to disembark.")
elif transporter.draw_arguments[transporter.loading_toggle_argument].value > transporter.disembark_embark_argument_toggle_argument_threshold:
continue
except Exception as e:
logger.error(f"Error in check_for_transport_embarker_or_disembark: {e}")
#############
#API SECTION#
#############
def on_api_startup(api: API):
global units_to_delete
logger.info("API started")
# Get all the units from the API. Force an update to get the latest units.
units = api.update_units()
# Initialize the list to hold units to delete
units_to_delete = []
generate_transport_units() #comment this if doing draw Args testing
#new_test_unit() # comment this if running normally, this is used only for getting draw args
def on_unit_alive_change(unit: Unit, value: bool):
global units_to_delete
if units_to_delete is None:
logger.error("units_to_delete is not initialized.")
return
# Check if the unit has been deleted
if value is False:
if unit in units_to_delete:
units_to_delete.remove(unit)
else:
pass
# unit_args = []
# exclusions_array = [1,102,103,11,12,17,278,279,280,281,282,283,284,286,287,288,289,290,337,37,39,393,399,4,40,41,42,448,487,488,6,77,99]
# async def check_args_changed():
# global unit_args
# units = api.get_units()
# for unit in units.values():
# for argument in unit.draw_arguments:
# if argument in unit_args:
# pass
# else:
# if argument.argument in exclusions_array:
# pass
# else:
# print(argument.argument, end=",")
# unit_args = unit.draw_arguments
# print("New loop")
# def new_test_unit():
# units = api.get_units()
# for unit in units.values():
# if unit.alive and unit.name in transport_types and not hasattr(unit, 'is_transport'):
# for i in range(500): #191
# unit.register_draw_argument(i)
# def check_arg_value():
# units = api.get_units()
# for unit in units.values():
# if unit.alive and unit.name in transport_types:
# unit.register_draw_argument(47) #191
# print(f"{unit.draw_arguments[0].argument} value is {unit.draw_arguments[0].value}")
async def on_api_update(api: API):
asyncio.create_task(check_for_loadable_units())
asyncio.create_task(load_loadable_units())
asyncio.create_task(check_for_unloadable_units())
asyncio.create_task(check_for_transport_embarker_or_disembark())
generate_transport_units()
#asyncio.create_task(check_args_changed())
#check_arg_value()
if __name__ == "__main__":
api = API()
api.register_on_startup_callback(on_api_startup)
api.register_on_update_callback(on_api_update)
api.run()

View File

@@ -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_indexes import DataIndexes
from data.data_types import LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset
from data.data_types import DrawArgument, LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset
from data.roes import ROES
from data.states import states
from utils.utils import enum_to_coalition
@@ -81,6 +81,10 @@ class Unit:
self.targeting_range = 0.0
self.aim_method_range = 0.0
self.acquisition_range = 0.0
self.cargo_weight = 0.0
self.draw_arguments: List[DrawArgument] = []
self.custom_string = ""
self.custom_integer = 0
self.previous_total_ammo = 0
self.total_ammo = 0
@@ -654,6 +658,34 @@ class Unit:
# Trigger callbacks for property change
if "airborne" in self.on_property_change_callbacks:
self._trigger_callback("airborne", self.airborne)
elif datum_index == DataIndexes.CARGO_WEIGHT.value:
cargo_weight = data_extractor.extract_float64()
if cargo_weight != self.cargo_weight:
self.cargo_weight = cargo_weight
# Trigger callbacks for property change
if "cargo_weight" in self.on_property_change_callbacks:
self._trigger_callback("cargo_weight", self.cargo_weight)
elif datum_index == DataIndexes.DRAW_ARGUMENTS.value:
draw_arguments = data_extractor.extract_draw_arguments()
if draw_arguments != self.draw_arguments:
self.draw_arguments = draw_arguments
# Trigger callbacks for property change
if "draw_arguments" in self.on_property_change_callbacks:
self._trigger_callback("draw_arguments", self.draw_arguments)
elif datum_index == DataIndexes.CUSTOM_STRING.value:
custom_string = data_extractor.extract_string()
if custom_string != self.custom_string:
self.custom_string = custom_string
# Trigger callbacks for property change
if "custom_string" in self.on_property_change_callbacks:
self._trigger_callback("custom_string", self.custom_string)
elif datum_index == DataIndexes.CUSTOM_INTEGER.value:
custom_integer = data_extractor.extract_uint32()
if custom_integer != self.custom_integer:
self.custom_integer = custom_integer
# Trigger callbacks for property change
if "custom_integer" in self.on_property_change_callbacks:
self._trigger_callback("custom_integer", self.custom_integer)
# --- API functions requiring ID ---
def set_path(self, path: List[LatLng]):
@@ -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):
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

@@ -114,16 +114,13 @@ if len(sys.argv) > 1:
database = json.load(f)
# Loads the loadout names
with open('unitPayloads.lua') as f:
with open('../unitPayloads.lua') as f:
lines = f.readlines()
unit_payloads = lua.decode("".join(lines).replace("Olympus.unitPayloads = ", "").replace("\n", ""))
# Loads the loadout roles
with open('payloadRoles.json') as f:
payloads_roles = json.load(f)
with open('pylonUsage.json') as f:
pylon_usage = json.load(f)
# Loop on all the units in the database
for unit_name in database:
@@ -157,15 +154,6 @@ if len(sys.argv) > 1:
}
database[unit_name]["loadouts"].append(empty_loadout)
# Add the available pylon usage
database[unit_name]["acceptedPayloads"] = {}
for pylon_name in pylon_usage[unit_name]:
pylon_data = pylon_usage[unit_name][pylon_name]
database[unit_name]["acceptedPayloads"][pylon_name] = {
"clsids": pylon_data,
"names": [find_weapon_name(clsid) for clsid in pylon_data]
}
# Loop on all the loadouts for that unit
for payload_name in unit_payloads[unit_name]:
payload_weapons = {}

View File

@@ -47,10 +47,8 @@ if len(sys.argv) > 1:
print(f"Warning, could not find {unit_name} in classes list. Skipping...")
continue
if not "acquisitionRange" in database[unit_name]:
database[unit_name]["acquisitionRange"] = unitmap[found_name].detection_range
if not "engagementRange" in database[unit_name]:
database[unit_name]["engagementRange"] = unitmap[found_name].threat_range
database[unit_name]["acquisitionRange"] = unitmap[found_name].detection_range
database[unit_name]["engagementRange"] = unitmap[found_name].threat_range
except Exception as e:
print(f"Could not find data for unitof type {unit_name}: {e}, skipping...")

View File

@@ -92,9 +92,7 @@ for filename in filenames:
src = tmp['payloads'].values()
else:
src = tmp['payloads']
print(f"Processing {filename} with {len(src)} payloads, detected unit name {tmp['unitType']}")
names[tmp['unitType']] = []
roles[tmp['unitType']] = {}
payloads[tmp['unitType']] = {}
@@ -131,22 +129,9 @@ for filename in filenames:
with open('payloadRoles.json', 'w') as f:
json.dump(roles, f, ensure_ascii = False, indent = 2)
with open('unitPayloads.lua', 'w') as f:
with open('../unitPayloads.lua', 'w') as f:
f.write("Olympus.unitPayloads = " + dump_lua(payloads))
# Iterate over the payloads and accumulate the pylon data
pylon_usage = {}
for unitType, unitPayloads in payloads.items():
pylon_usage[unitType] = {}
for payloadName, pylons in unitPayloads.items():
for pylonID, pylonData in pylons.items():
# Keep track of what CLSIDs are used on each pylon
clsid = pylonData['CLSID']
if pylonID not in pylon_usage[unitType]:
pylon_usage[unitType][pylonID] = []
if clsid not in pylon_usage[unitType][pylonID]:
pylon_usage[unitType][pylonID].append(clsid)
# Save the pylon usage data to a JSON file
with open('pylonUsage.json', 'w') as f:
json.dump(pylon_usage, f, ensure_ascii=False, indent=2)

View File

@@ -3773,23 +3773,6 @@
"1": 29
}
},
"F4U-1D": {
"HVAR x 8": {
"1": 32,
"2": 31,
"3": 30
},
"Bomb x 2, HVAR x 4": {
"1": 32,
"2": 31,
"3": 30
},
"Tiny Tim x2, HVAR x 4": {
"1": 32,
"2": 31,
"3": 30
}
},
"F/A-18A": {
"GBU-16*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3": {
"1": 33
@@ -4743,15 +4726,6 @@
"AEROBATIC": {}
},
"Mirage-F1B": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -4807,15 +4781,6 @@
}
},
"Mirage-F1BD": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -4865,15 +4830,6 @@
}
},
"Mirage-F1BE": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -4953,15 +4909,6 @@
}
},
"Mirage-F1BQ": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5011,15 +4958,6 @@
}
},
"Mirage-F1C-200": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5075,15 +5013,6 @@
}
},
"Mirage-F1C": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5139,15 +5068,6 @@
}
},
"Mirage-F1CE": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5227,15 +5147,6 @@
}
},
"Mirage-F1CG": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM-9 JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5297,15 +5208,6 @@
}
},
"Mirage-F1CH": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5361,15 +5263,6 @@
}
},
"Mirage-F1CJ": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5425,15 +5318,6 @@
}
},
"Mirage-F1CK": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5489,15 +5373,6 @@
}
},
"Mirage-F1CR": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I": {
"1": 10
},
@@ -5535,15 +5410,6 @@
}
},
"Mirage-F1CT": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5593,15 +5459,6 @@
}
},
"Mirage-F1CZ": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5657,15 +5514,6 @@
}
},
"Mirage-F1DDA": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5715,15 +5563,6 @@
}
},
"Mirage-F1ED": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic II, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5773,15 +5612,6 @@
}
},
"Mirage-F1EDA": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5831,15 +5661,6 @@
}
},
"Mirage-F1EE": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5925,15 +5746,6 @@
}
},
"Mirage-F1EH": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5989,15 +5801,6 @@
}
},
"Mirage-F1EQ": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -6047,15 +5850,6 @@
}
},
"Mirage-F1JA": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*Python III, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -6102,15 +5896,6 @@
}
},
"Mirage-F1M-CE": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -6184,15 +5969,6 @@
}
},
"Mirage-F1M-EE": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -8369,7 +8145,7 @@
"UB-32*2,Fuel*3": {
"1": 32
},
"Kh-59M*2,R-60M*2": {
"Kh-59M*2,R-60M*2,Fuel": {
"1": 33
},
"S-25*4": {

File diff suppressed because it is too large Load Diff

View File

@@ -1780,8 +1780,8 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[1] = {["CLSID"]="{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}"},
[4] = {["CLSID"]="{C-101-DEFA553}"}},
["2*AIM-9P, 2*BELOUGA, DEFA 553 CANNON"]={[4] = {["CLSID"]="{C-101-DEFA553}"},
[5] = {["CLSID"]="{BLG66_AC}"},
[3] = {["CLSID"]="{BLG66_AC}"},
[5] = {["CLSID"]="{BLG66_BELOUGA}"},
[3] = {["CLSID"]="{BLG66_BELOUGA}"},
[7] = {["CLSID"]="{9BFD8C90-F7AE-4e90-833B-BFD0CED0E536}"},
[1] = {["CLSID"]="{9BFD8C90-F7AE-4e90-833B-BFD0CED0E536}"}},
["2*AIM9-P, 2*SEA EAGLE, DEFA-553 CANNON"]={[7] = {["CLSID"]="{9BFD8C90-F7AE-4e90-833B-BFD0CED0E536}"},
@@ -1797,8 +1797,8 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
["2*AIM-9M, AN-M3 CANNON"]={[7] = {["CLSID"]="{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}"},
[1] = {["CLSID"]="{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}"},
[4] = {["CLSID"]="{AN-M3}"}},
["2*BELOUGA,2*BDU-33, DEFA-553 CANNON"]={[6] = {["CLSID"]="{BLG66_AC}"},
[2] = {["CLSID"]="{BLG66_AC}"},
["2*BELOUGA,2*BDU-33, DEFA-553 CANNON"]={[6] = {["CLSID"]="{BLG66_BELOUGA}"},
[2] = {["CLSID"]="{BLG66_BELOUGA}"},
[3] = {["CLSID"]="CBLS-200"},
[5] = {["CLSID"]="CBLS-200"},
[4] = {["CLSID"]="{C-101-DEFA553}"}},
@@ -1820,11 +1820,11 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
["2*R.550 MAGIC, DEFA 553 CANNON (IV)"]={[4] = {["CLSID"]="{C-101-DEFA553}"},
[1] = {["CLSID"]="{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}"},
[7] = {["CLSID"]="{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}"}},
["2*BELOUGA, 2*BR-500, DEFA 553 CANNON"]={[2] = {["CLSID"]="{BLG66_AC}"},
["2*BELOUGA, 2*BR-500, DEFA 553 CANNON"]={[2] = {["CLSID"]="{BLG66_BELOUGA}"},
[3] = {["CLSID"]="BR_500"},
[4] = {["CLSID"]="{C-101-DEFA553}"},
[5] = {["CLSID"]="BR_500"},
[6] = {["CLSID"]="{BLG66_AC}"}},
[6] = {["CLSID"]="{BLG66_BELOUGA}"}},
["2*AIM-9M, DEFA 553 CANNON (IV)"]={[7] = {["CLSID"]="{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}"},
[1] = {["CLSID"]="{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}"},
[4] = {["CLSID"]="{C-101-DEFA553}"}},
@@ -1837,8 +1837,8 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
["2*AIM-9M ,2*BELOUGA,2*BIN-200, AN-M3 CANNON"]={[4] = {["CLSID"]="{AN-M3}"},
[3] = {["CLSID"]="BIN_200"},
[5] = {["CLSID"]="BIN_200"},
[6] = {["CLSID"]="{BLG66_AC}"},
[2] = {["CLSID"]="{BLG66_AC}"},
[6] = {["CLSID"]="{BLG66_BELOUGA}"},
[2] = {["CLSID"]="{BLG66_BELOUGA}"},
[7] = {["CLSID"]="{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}"},
[1] = {["CLSID"]="{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}"}},
["2*AIM-9M, 2*LAU 68, 2*MK-82, DEFA 553 CANNON"]={[4] = {["CLSID"]="{C-101-DEFA553}"},
@@ -3346,13 +3346,13 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[10] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"}},
["GUIDED: GBU-8 HOBOS*2, Aim-7E2*3, Aim-9L*4, AN/AVQ-23 Pave Spike*1, ALE-40 (30-60)*1, Sargent Fl. Fuel Tank 600 GAL*1"]={[13] = {["CLSID"]="{GBU_8_B}"},
["GUIDED: GBU-8 HOBOS*2, Aim-7E2*3, Aim-9L*4, AN/AVQ-23 Pave Spike*1, ALE-40 (30-60)*1, Sargent Fl. Fuel Tank 600 GAL*1"]={[13] = {["CLSID"]="{HB_F4E_GBU_8}"},
[9] = {["CLSID"]="{HB_F4E_AIM-7E-2}"},
[8] = {["CLSID"]="{HB_F4E_AIM-7E-2}"},
[7] = {["CLSID"]="{F4_SARGENT_TANK_600_GAL}"},
[6] = {["CLSID"]="{HB_PAVE_SPIKE_FAST_ON_ADAPTER_IN_AERO7}"},
[5] = {["CLSID"]="{HB_F4E_AIM-7E-2}"},
[1] = {["CLSID"]="{GBU_8_B}"},
[1] = {["CLSID"]="{HB_F4E_GBU_8}"},
[14] = {["CLSID"]="{HB_ALE_40_30_60}"},
[12] = {["CLSID"]="{AIM-9L}"},
[10] = {["CLSID"]="{AIM-9L}"},
@@ -3907,15 +3907,15 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[4] = {["CLSID"]="{AIM-9L}"},
[2] = {["CLSID"]="{AIM-9L}"},
[14] = {["CLSID"]="{HB_ALE_40_30_60}"}},
["GUIDED: GBU-8 HOBOS*4, Aim-7E2*3, AN/AVQ-23 Pave Spike*1, ALE-40 (30-60)*1, Sargent Fl. Fuel Tank 600 GAL*1"]={[13] = {["CLSID"]="{GBU_8_B}"},
["GUIDED: GBU-8 HOBOS*4, Aim-7E2*3, AN/AVQ-23 Pave Spike*1, ALE-40 (30-60)*1, Sargent Fl. Fuel Tank 600 GAL*1"]={[13] = {["CLSID"]="{HB_F4E_GBU_8}"},
[9] = {["CLSID"]="{HB_F4E_AIM-7E-2}"},
[8] = {["CLSID"]="{HB_F4E_AIM-7E-2}"},
[7] = {["CLSID"]="{F4_SARGENT_TANK_600_GAL}"},
[6] = {["CLSID"]="{HB_PAVE_SPIKE_FAST_ON_ADAPTER_IN_AERO7}"},
[5] = {["CLSID"]="{HB_F4E_AIM-7E-2}"},
[1] = {["CLSID"]="{GBU_8_B}"},
[11] = {["CLSID"]="{GBU_8_B}"},
[3] = {["CLSID"]="{GBU_8_B}"},
[1] = {["CLSID"]="{HB_F4E_GBU_8}"},
[11] = {["CLSID"]="{HB_F4E_GBU_8}"},
[3] = {["CLSID"]="{HB_F4E_GBU_8}"},
[14] = {["CLSID"]="{HB_ALE_40_30_60}"},
[12] = {["CLSID"]="<CLEAN>"},
[10] = {["CLSID"]="<CLEAN>"},
@@ -4504,8 +4504,8 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[6] = {["CLSID"]="{HB_PAVE_SPIKE_FAST_ON_ADAPTER_IN_AERO7}"},
[5] = {["CLSID"]="{HB_F4E_AIM-7E-2}"},
[1] = {["CLSID"]="{F4_SARGENT_TANK_370_GAL}"},
[11] = {["CLSID"]="{GBU_8_B}"},
[3] = {["CLSID"]="{GBU_8_B}"},
[11] = {["CLSID"]="{HB_F4E_GBU_8}"},
[3] = {["CLSID"]="{HB_F4E_GBU_8}"},
[14] = {["CLSID"]="{HB_ALE_40_30_60}"},
[12] = {["CLSID"]="<CLEAN>"},
[10] = {["CLSID"]="<CLEAN>"},
@@ -5084,8 +5084,8 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[6] = {["CLSID"]="{HB_PAVE_SPIKE_FAST_ON_ADAPTER_IN_AERO7}"},
[5] = {["CLSID"]="{HB_F4E_AIM-7E-2}"},
[1] = {["CLSID"]="{34759BBC-AF1E-4AEE-A581-498FF7A6EBCE}"},
[11] = {["CLSID"]="{GBU_8_B}"},
[3] = {["CLSID"]="{GBU_8_B}"},
[11] = {["CLSID"]="{HB_F4E_GBU_8}"},
[3] = {["CLSID"]="{HB_F4E_GBU_8}"},
[14] = {["CLSID"]="{HB_ALE_40_30_60}"},
[12] = {["CLSID"]="<CLEAN>"},
[10] = {["CLSID"]="<CLEAN>"},
@@ -5131,8 +5131,8 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[4] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[1] = {["CLSID"]="{LAU_34_AGM_45A}"},
[11] = {["CLSID"]="{GBU_8_B}"},
[3] = {["CLSID"]="{GBU_8_B}"}},
[11] = {["CLSID"]="{HB_F4E_GBU_8}"},
[3] = {["CLSID"]="{HB_F4E_GBU_8}"}},
["IRON: M-117*11, Aim-7E2*3, ALQ-131 ECM*1, ALE-40 (30-60)*1, Sargent Fletcher Fuel Tank 370 GAL*2"]={[13] = {["CLSID"]="{F4_SARGENT_TANK_370_GAL_R}"},
[9] = {["CLSID"]="{HB_F4E_AIM-7E-2}"},
[8] = {["CLSID"]="{HB_F4E_AIM-7E-2}"},
@@ -6617,26 +6617,6 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{F14-300gal}"},
[2] = {["CLSID"]="{SHOULDER AIM_54A_Mk60 L}"},
[1] = {["CLSID"]="{LAU-138 wtip - AIM-9M}"}}},
["F4U-1D"]={["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}"},
[8] = {["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}"}},
["Bomb x 2, HVAR x 4"]={[7] = {["CLSID"]="{AN-M64}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{AN-M64}"}},
["Tiny Tim x2, HVAR x 4"]={[7] = {["CLSID"]="{Tiny_Tim_Corsair}"},
[8] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[3] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[4] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[9] = {["CLSID"]="{HVAR_USN_Mk28_Mod4_Corsair}"},
[5] = {["CLSID"]="{Tiny_Tim_Corsair}"}}},
["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}"},
[3] = {["CLSID"]="{EFEC8200-B922-11d7-9897-000476191836}"},
@@ -8122,14 +8102,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[5] = {["CLSID"]="{R-3S}"},
[6] = {["CLSID"]="{ASO-2}"}},
["AEROBATIC"]={[7] = {["CLSID"]="{SMOKE_WHITE}"}}},
["Mirage-F1B"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1B"]={["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -8220,14 +8193,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1BD"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1BD"]={["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{S530F}"},
[3] = {["CLSID"]="{S530F}"},
@@ -8311,14 +8277,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1BE"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
["Mirage-F1BE"]={["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
[1] = {["CLSID"]="{AIM-9JULI}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -8432,14 +8391,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[5] = {["CLSID"]="{S530F}"},
[3] = {["CLSID"]="{S530F}"},
[4] = {["CLSID"]="PTB-1200-F1"}}},
["Mirage-F1BQ"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1BQ"]={["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{S530F}"},
[3] = {["CLSID"]="{S530F}"},
@@ -8523,14 +8475,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1C-200"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1C-200"]={["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -8621,14 +8566,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1C"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1C"]={["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -8719,14 +8657,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1CE"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
["Mirage-F1CE"]={["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
[1] = {["CLSID"]="{AIM-9JULI}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -8840,14 +8771,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[5] = {["CLSID"]="{S530F}"},
[3] = {["CLSID"]="{S530F}"},
[4] = {["CLSID"]="PTB-1200-F1"}}},
["Mirage-F1CG"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*AIM-9 JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
["Mirage-F1CG"]={["2*AIM-9 JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
[1] = {["CLSID"]="{AIM-9JULI}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -8945,14 +8869,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1CH"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1CH"]={["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -9043,14 +8960,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1CJ"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1CJ"]={["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -9141,14 +9051,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1CK"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1CK"]={["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -9239,14 +9142,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1CR"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1CR"]={["2*R550 Magic I"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"}},
["2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
@@ -9314,14 +9210,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1CT"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1CT"]={["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{S530F}"},
[3] = {["CLSID"]="{S530F}"},
@@ -9405,14 +9294,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1CZ"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1CZ"]={["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -9503,14 +9385,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1DDA"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1DDA"]={["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{S530F}"},
[3] = {["CLSID"]="{S530F}"},
@@ -9594,14 +9469,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1ED"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic II, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}"},
["Mirage-F1ED"]={["2*R550 Magic II, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}"},
[1] = {["CLSID"]="{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}"},
[5] = {["CLSID"]="{S530F}"},
[3] = {["CLSID"]="{S530F}"},
@@ -9685,14 +9553,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1EDA"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1EDA"]={["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{S530F}"},
[3] = {["CLSID"]="{S530F}"},
@@ -9776,14 +9637,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1EE"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
["Mirage-F1EE"]={["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
[1] = {["CLSID"]="{AIM-9JULI}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -9903,14 +9757,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{S530F}"},
[4] = {["CLSID"]="PTB-1200-F1"},
[6] = {["CLSID"]="BARAX_ECM"}}},
["Mirage-F1EH"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1EH"]={["2*R550 Magic I, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -10001,14 +9848,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1EQ"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1EQ"]={["2*R550 Magic I, 2*S530F, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[5] = {["CLSID"]="{S530F}"},
[3] = {["CLSID"]="{S530F}"},
@@ -10092,14 +9932,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1JA"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*R550 Magic I, 2*Python III, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
["Mirage-F1JA"]={["2*R550 Magic I, 2*Python III, 1*Fuel Tank"]={[7] = {["CLSID"]="{R550_Magic_1}"},
[1] = {["CLSID"]="{R550_Magic_1}"},
[2] = {["CLSID"]="DIS_PL-8B"},
[6] = {["CLSID"]="DIS_PL-8B"},
@@ -10183,14 +10016,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[3] = {["CLSID"]="{BLU107B_DURANDAL}"},
[5] = {["CLSID"]="{BLU107B_DURANDAL}"},
[4] = {["CLSID"]="{CLB4_BLU107}"}}},
["Mirage-F1M-CE"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
["Mirage-F1M-CE"]={["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
[1] = {["CLSID"]="{AIM-9JULI}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -10299,14 +10125,7 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[5] = {["CLSID"]="{S530F}"},
[3] = {["CLSID"]="{S530F}"},
[4] = {["CLSID"]="PTB-1200-F1"}}},
["Mirage-F1M-EE"]={["Clean"]={[1] = {["CLSID"]="<CLEAN>"},
[2] = {["CLSID"]="<CLEAN>"},
[3] = {["CLSID"]="<CLEAN>"},
[4] = {["CLSID"]="<CLEAN>"},
[5] = {["CLSID"]="<CLEAN>"},
[6] = {["CLSID"]="<CLEAN>"},
[7] = {["CLSID"]="<CLEAN>"}},
["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
["Mirage-F1M-EE"]={["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank"]={[7] = {["CLSID"]="{AIM-9JULI}"},
[1] = {["CLSID"]="{AIM-9JULI}"},
[5] = {["CLSID"]="{R530F_IR}"},
[3] = {["CLSID"]="{R530F_IR}"},
@@ -13317,11 +13136,11 @@ Olympus.unitPayloads = {["A-10A"]={["MK-84*2 , LAU-68*2 , AGM-65K*2"]={[1] = {["
[5] = {["CLSID"]="{16602053-4A12-40A2-B214-AB60D481B20E}"},
[7] = {["CLSID"]="{7D7EC917-05F6-49D4-8045-61FC587DD019}"},
[8] = {["CLSID"]="{637334E4-AB5A-47C0-83A6-51B7F1DF3CD5}"}},
["Kh-59M*2,R-60M*2"]={[1] = {["CLSID"]="{APU-60-1_R_60M}"},
["Kh-59M*2,R-60M*2,Fuel"]={[1] = {["CLSID"]="{APU-60-1_R_60M}"},
[2] = {["CLSID"]="{40AB87E8-BEFB-4D85-90D9-B2753ACF9514}"},
[5] = {["CLSID"]="{16602053-4A12-40A2-B214-AB60D481B20E}"},
[7] = {["CLSID"]="{40AB87E8-BEFB-4D85-90D9-B2753ACF9514}"},
[8] = {["CLSID"]="{APU-60-1_R_60M}"},
[4] = {["CLSID"]="{APK_9}"}},
[8] = {["CLSID"]="{APU-60-1_R_60M}"}},
["S-25*4"]={[1] = {["CLSID"]="{A0648264-4BC0-4EE8-A543-D119F6BA4257}"},
[2] = {["CLSID"]="{A0648264-4BC0-4EE8-A543-D119F6BA4257}"},
[7] = {["CLSID"]="{A0648264-4BC0-4EE8-A543-D119F6BA4257}"},