DCSOlympus/scripts/lua/backend/OlympusCommand.lua
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

1766 lines
56 KiB
Lua

local version = "{{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}"
local debug = false -- True enables debug printing using DCS messages
-- .dll related variables
Olympus.OlympusDLL = nil
Olympus.DLLsloaded = false
-- Logger reference
Olympus.log = mist.Logger:new("Olympus", 'info')
-- Data structures for transfer to .dll
Olympus.missionData = {}
Olympus.unitsData = {}
Olympus.weaponsData = {}
Olympus.drawingsByLayer = {}
Olympus.executionResults = {}
-- Units data structures
Olympus.unitCounter = 1 -- Counter to generate unique names
Olympus.cloneDatabase = {} -- Database of spawn options, used for units cloning
Olympus.unitIndex = 0 -- Counter used to spread the computational load of data retrievial from DCS
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
Olympus.weapons = {} -- Table holding references to all the currently existing weapons
-- Spots (laser/IR) data
Olympus.spots = {}
Olympus.spotsCounter = 1
-- Miscellaneous initializations
Olympus.missionStartTime = DCS.getRealTime()
Olympus.napalmCounter = 1
Olympus.fireCounter = 1
-- Load the lua file system
local lfs = require('lfs')
------------------------------------------------------------------------------------------------------
-- Olympus functions
------------------------------------------------------------------------------------------------------
-- Print a debug message if the debug option is true
function Olympus.debug(message, displayFor)
if debug == true then
Olympus.log:info(message)
trigger.action.outText(message, displayFor)
end
end
-- Print a notify message
function Olympus.notify(message, displayFor)
Olympus.log:info(message)
trigger.action.outText(message, displayFor)
end
-- Loads the olympus .dll
function Olympus.loadDLLs()
-- Add the .dll paths
package.cpath = package.cpath..';'..Olympus.instancePath..'?.dll;'
local status
status, Olympus.OlympusDLL = pcall(require, 'olympus')
if status then
return true
else
return false
end
end
-- Gets a unit class reference from a given ObjectID (the ID used by Olympus for unit referencing)
function Olympus.getUnitByID(ID)
return Olympus.units[ID];
end
-- Gets the ID of the first country that belongs to a coalition
function Olympus.getCountryIDByCoalition(coalitionString)
for countryName, countryId in pairs(country.id) do
if coalition.getCountryCoalition(countryId) == Olympus.getCoalitionIDByCoalition(coalitionString) then
return countryId
end
end
return 0
end
-- Gets the coalition ID of a coalition
function Olympus.getCoalitionIDByCoalition(coalitionString)
local coalitionID = 0
if coalitionString == "red" then
coalitionID = 1
elseif coalitionString == "blue" then
coalitionID = 2
end
return coalitionID
end
-- Gets the coalition name from the coalition ID
function Olympus.getCoalitionByCoalitionID(coalitionID)
local coalitionString = "neutral"
if coalitionID == 1 then
coalitionString = "red"
elseif coalitionID == 2 then
coalitionString = "blue"
end
return coalitionString
end
-- Builds a valid enroute task depending on the provided options
function Olympus.buildEnrouteTask(options)
local task = nil
-- Engage specific target by ID. Checks if target exists.
if options['id'] == 'EngageUnit' and options['targetID'] then
local target = Olympus.getUnitByID(options['targetID'])
if target and target:isExist() then
task = {
id = 'EngageUnit',
params = {
unitId = options['targetID'],
}
}
end
-- Start being an active tanker
elseif options['id'] == 'Tanker' then
task = {
id = 'Tanker',
params = {},
}
-- Start being an active AWACS
elseif options['id'] == 'AWACS' then
task = {
id = 'AWACS',
params = {},
}
end
return task
end
-- Builds a valid main task depending on the provided options
function Olympus.buildTask(groupName, options)
local task = nil
local group = Group.getByName(groupName)
-- Combo tasks require nested tables
if (Olympus.isArray(options)) then
local tasks = {}
for idx, subOptions in pairs(options) do
tasks[idx] = Olympus.buildTask(groupName, subOptions) or Olympus.buildEnrouteTask(subOptions)
end
task = {
id = 'ComboTask',
params = {
tasks = tasks
}
}
Olympus.debug(Olympus.serializeTable(task), 30)
else
-- Follow a unit in formation with a given offset
if options['id'] == 'FollowUnit' and options['leaderID'] and options['offset'] then
local leader = Olympus.getUnitByID(options['leaderID'])
if leader and leader:isExist() then
task = {
id = 'Follow',
params = {
groupId = leader:getGroup():getID(),
pos = options['offset'],
lastWptIndexFlag = false,
lastWptIndex = 1
}
}
end
-- Go refuel to the nearest tanker. If the unit can't refuel it will RTB
elseif options['id'] == 'Refuel' then
task = {
id = 'Refueling',
params = {}
}
-- Orbit in place at a given altitude and with a given pattern
elseif options['id'] == 'Orbit' then
task = {
id = 'Orbit',
params = {
pattern = options['pattern'] or "Circle"
}
}
if options['pattern'] == 'Race-Track' then
local heading = options['heading'] or 0
local length = options['length'] or 20000
if group ~= nil then
local groupPos = mist.getLeadPos(group)
task['params']['point'] = {x = groupPos.x, y = groupPos.z}
task['params']['point2'] = {x = groupPos.x + math.cos(heading) * length, y = groupPos.z + math.sin(heading) * length}
end
end
-- Compute the altitude depending on the altitude type
if options['altitude'] then
if options ['altitudeType'] then
if options ['altitudeType'] == "AGL" then
local groundHeight = 0
if group ~= nil then
local groupPos = mist.getLeadPos(group)
groundHeight = land.getHeight({x = groupPos.x, y = groupPos.z})
end
task['params']['altitude'] = groundHeight + options['altitude']
else
task['params']['altitude'] = options['altitude']
end
else
task['params']['altitude'] = options['altitude']
end
end
-- Compute the speed depending on the speed type. CAS calculation is only available if the altitude is also available
if options['speed'] then
-- Simplified formula to compute CAS from GS
local speed = options['speed']
if options['speedType'] and options['speedType'] == "CAS" and task['params']['altitude'] then
speed = speed * (1 + 0.02 * task['params']['altitude'] / 0.3048 / 1000)
end
task['params']['speed'] = speed
end
-- Bomb a specific location
elseif options['id'] == 'Bombing' and options['lat'] and options['lng'] then
local point = coord.LLtoLO(options['lat'], options['lng'], 0)
task = {
id = 'Bombing',
params = {
point = {x = point.x, y = point.z},
attackQty = 1
}
}
-- Perform carpet bombing at a specific location
elseif options['id'] == 'CarpetBombing' and options['lat'] and options['lng'] then
local point = coord.LLtoLO(options['lat'], options['lng'], 0)
task = {
id = 'CarpetBombing',
params = {
x = point.x,
y = point.z,
carpetLength = 1000,
attackType = 'Carpet',
expend = "All",
attackQty = 1,
attackQtyLimit = true
}
}
-- Fire at a specific point
elseif options['id'] == 'FireAtPoint' and options['lat'] and options['lng'] and options['radius'] then
local point = coord.LLtoLO(options['lat'], options['lng'], 0)
local expendQtyEnabled = false
local expendQty = 0
if options['expendQty'] then
expendQtyEnabled = true
expendQty = options['expendQty']
end
if options['alt'] then
task = {
id = 'FireAtPoint',
params = {
point = {x = point.x, y = point.z},
radius = options['radius'],
altitude = options['alt'],
alt_type = 0, -- ASL
expendQtyEnabled = expendQtyEnabled,
expendQty = expendQty
}
}
else
task = {
id = 'FireAtPoint',
params = {
point = {x = point.x, y = point.z},
radius = options['radius'],
expendQtyEnabled = expendQtyEnabled,
expendQty = expendQty
}
}
end
-- Land at a specific point
elseif options['id'] == 'LandAtPoint' then
local point = coord.LLtoLO(options['lat'], options['lng'], 0)
task = {
id = 'Land',
params = {
point = {x = point.x, y = point.z},
}
}
-- Attack unit
elseif options['id'] == 'AttackUnit' and options['unitID'] then
task = {
id = 'AttackUnit',
params = {
unitId = options['unitID'],
}
}
end
end
return task
end
-- Move a unit. Since many tasks in DCS are Enroute tasks, this function is an important way to control the unit AI
function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedType, category, taskOptions)
Olympus.debug("Olympus.move " .. groupName .. " (" .. lat .. ", " .. lng ..") " .. altitude .. "m " .. altitudeType .. " ".. speed .. "m/s " .. category .. " " .. Olympus.serializeTable(taskOptions), 2)
local group = Group.getByName(groupName)
if group ~= nil then
if category == "Aircraft" then
local startPoint = mist.getLeadPos(group)
local endPoint = coord.LLtoLO(lat, lng, 0)
-- 'AGL' mode does not appear to work in the buildWP function. This is a crude approximation
if altitudeType == "AGL" then
altitude = land.getHeight({x = endPoint.x, y = endPoint.z}) + altitude
end
-- Simplified formula to compute CAS from GS
if speedType == "CAS" then
speed = speed * (1 + 0.02 * altitude / 0.3048 / 1000)
end
-- Create the path
local path = {
[1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'),
[2] = mist.fixedWing.buildWP(endPoint, turningPoint, speed, altitude, 'BARO')
}
-- If a task exists assign it to the controller
if taskOptions then
local task = Olympus.buildEnrouteTask(taskOptions)
if task then
path[1].task = task
path[2].task = task
end
end
-- Assign the mission task to the controller
local missionTask = {
id = 'Mission',
params = {
route = {
points = mist.utils.deepCopy(path),
},
},
}
local groupCon = group:getController()
if groupCon then
groupCon:setTask(missionTask)
end
Olympus.debug("Olympus.move executed successfully on Aircraft", 2)
elseif category == "Helicopter" then
local startPoint = mist.getLeadPos(group)
local endPoint = coord.LLtoLO(lat, lng, 0)
-- 'AGL' mode does not appear to work in the buildWP function. This is a crude approximation
if altitudeType == "AGL" then
altitude = land.getHeight({x = endPoint.x, y = endPoint.z}) + altitude
end
-- Simplified formula to compute CAS from GS
if speedType == "CAS" then
speed = speed * (1 + 0.02 * altitude / 0.3048 / 1000)
end
-- Create the path
local path = {
[1] = mist.heli.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'),
[2] = mist.heli.buildWP(endPoint, turningPoint, speed, altitude, 'BARO')
}
-- If a task exists assign it to the controller
if taskOptions then
local task = Olympus.buildEnrouteTask(taskOptions)
if task then
path[1].task = task
path[2].task = task
end
end
-- Assign the mission task to the controller
local missionTask = {
id = 'Mission',
params = {
route = {
points = mist.utils.deepCopy(path),
},
},
}
local groupCon = group:getController()
if groupCon then
groupCon:setTask(missionTask)
end
Olympus.debug("Olympus.move executed successfully on Helicopter", 2)
elseif category == "GroundUnit" then
local endPoint = coord.LLtoLO(lat, lng, 0)
local action = "Off Road"
local disableRoads = true
if taskOptions and taskOptions['id'] == 'FollowRoads' and taskOptions['value'] == true then
action = "On Road"
disableRoads = false
end
missionTask = {
id = 'Mission',
params = {
route = {
points = {
[1] = {
type = "Turning Point",
action = action,
disableRoads = disableRoads,
x = endPoint.x,
y = endPoint.z,
speed = speed,
speed_locked = false,
ETA_locked = false,
name = 'Mission1',
},
[2] = {
type = "Turning Point",
action = action,
disableRoads = disableRoads,
x = endPoint.x,
y = endPoint.z,
speed = speed,
speed_locked = false,
ETA_locked = false,
name = 'Mission1',
},
}
},
}
}
local groupCon = group:getController()
if groupCon then
groupCon:setTask(missionTask)
end
Olympus.debug("Olympus.move executed successfully on GroundUnit", 2)
elseif category == "NavyUnit" then
local startPoint = mist.getLeadPos(group)
local endPoint = coord.LLtoLO(lat, lng, 0)
local bearing = math.atan2(endPoint.z - startPoint.z, endPoint.x - startPoint.x)
vars = {
group = group,
point = endPoint,
heading = bearing,
speed = speed
}
mist.groupToRandomPoint(vars)
Olympus.debug("Olympus.move executed successfully on NavyUnit", 2)
else
Olympus.debug("Olympus.move not implemented yet for " .. category, 2)
end
else
Olympus.debug("Error in Olympus.move " .. groupName, 2)
end
end
-- Creates a simple smoke on the ground
function Olympus.smoke(color, lat, lng)
Olympus.debug("Olympus.smoke " .. color .. " (" .. lat .. ", " .. lng ..")", 2)
local colorEnum = nil
if color == "green" then
colorEnum = trigger.smokeColor.Green
elseif color == "red" then
colorEnum = trigger.smokeColor.Red
elseif color == "white" then
colorEnum = trigger.smokeColor.White
elseif color == "orange" then
colorEnum = trigger.smokeColor.Orange
elseif color == "blue" then
colorEnum = trigger.smokeColor.Blue
end
trigger.action.smoke(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)), colorEnum)
end
-- Creates an explosion on the ground
function Olympus.explosion(intensity, explosionType, lat, lng, alt)
Olympus.debug("Olympus.explosion " .. explosionType .. " " .. intensity .. " (" .. lat .. ", " .. lng .. ")", 2)
local vec3 = nil
if alt ~= nil then
vec3 = coord.LLtoLO(lat, lng, alt)
else
vec3 = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng))
end
if explosionType == "normal" then
trigger.action.explosion(vec3, intensity)
elseif explosionType == "phosphorous" then
Olympus.phosphorous(vec3)
elseif explosionType == "napalm" then
Olympus.napalm(vec3)
elseif explosionType == "secondary" then
Olympus.secondaries(vec3)
elseif explosionType == "fire" then
Olympus.createFire(vec3)
elseif explosionType == "depthCharge" then
end
end
function Olympus.phosphorous(vec3)
trigger.action.explosion(vec3, 1)
for i = 1,math.random(3, 10) do
angle = mist.utils.toRadian((math.random(1, 360)))
local randVec = mist.utils.makeVec3GL((mist.getRandPointInCircle(vec3, 5, 1, 0, 360)))
trigger.action.signalFlare(randVec, 2, angle)
end
end
function Olympus.napalm(vec3)
local napeName = "napalmStrike" .. Olympus.napalmCounter
Olympus.napalmCounter = Olympus.napalmCounter + 1
mist.dynAddStatic(
{
country = 20,
category = 'Fortifications',
hidden = true,
name = napeName,
type ="Fuel tank",
x = vec3.x,
y = vec3.z,
heading = 0,
} -- end of function
)
timer.scheduleFunction(Olympus.explodeNapalm, vec3, timer.getTime() + 0.1)
timer.scheduleFunction(Olympus.removeNapalm, napeName, timer.getTime() + 0.12)
end
function Olympus.explodeNapalm(vec3)
trigger.action.explosion(vec3, 10)
end
function Olympus.removeNapalm(staticName)
StaticObject.getByName(staticName):destroy()
end
function Olympus.createFire(vec3)
local smokeName = "smokeName" .. Olympus.fireCounter
Olympus.fireCounter = Olympus.fireCounter + 1
trigger.action.effectSmokeBig(vec3, 2 , 1, smokeName)
trigger.action.explosion(vec3, 1) -- looks wierd to spawn in on flat land without this
timer.scheduleFunction(Olympus.removeFire, smokeName, timer.getTime() + 20)
end
function Olympus.removeFire (smokeName)
trigger.action.effectSmokeStop(smokeName)
end
function Olympus.secondaries(vec3)
Olympus.randomDebries(vec3)
--trigger.action.explosion(vec3, 1)
--for i = 1, 10 do
-- timer.scheduleFunction(Olympus.randomDebries, vec3, timer.getTime() + math.random(0, 180))
--end
end
function Olympus.randomDebries(vec3)
trigger.action.explosion(vec3, 1)
for i = 1,math.random(3, 10) do
angle = mist.utils.toRadian((math.random(1, 360)))
local randVec = mist.utils.makeVec3GL((mist.getRandPointInCircle(vec3, 5, 1, 0, 360)))
trigger.action.signalFlare(randVec, 3, angle)
end
end
-- Shines a laser from a unit to a point
function Olympus.fireLaser(ID, code, lat, lng)
Olympus.debug("Olympus.fireLaser " .. ID .. " -> (" .. lat .. ", " .. lng .. ") code " .. code, 2)
local vec3 = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng))
local unit = Olympus.getUnitByID(ID)
if unit ~= nil and unit:isExist() then
local spot = Spot.createLaser(unit, {x = 0, y = 1, z = 0}, vec3, code)
Olympus.spotsCounter = Olympus.spotsCounter + 1
Olympus.spots[Olympus.spotsCounter] = {
type = "laser",
object = spot,
sourceUnitID = ID,
targetPosition = {
lat = lat,
lng = lng
},
active = true,
code = code
}
end
end
-- Shines a infrared light from a unit to a point
function Olympus.fireInfrared(ID, lat, lng)
Olympus.debug("Olympus.fireInfrared " .. ID .. " -> (" .. lat .. ", " .. lng .. ")", 2)
local vec3 = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng))
local unit = Olympus.getUnitByID(ID)
if unit ~= nil and unit:isExist() then
local spot = Spot.createInfraRed(unit, {x = 0, y = 1, z = 0}, vec3)
Olympus.spotsCounter = Olympus.spotsCounter + 1
Olympus.spots[Olympus.spotsCounter] = {
type = "infrared",
object = spot,
sourceUnitID = ID,
targetPosition = {
lat = lat,
lng = lng
},
active = true
}
end
end
-- Set new laser code
function Olympus.setLaserCode(spotID, code)
Olympus.debug("Olympus.setLaserCode " .. spotID .. " -> " .. code, 2)
local spot = Olympus.spots[spotID]
if spot ~= nil and spot.type == "laser" then
spot.object:setCode(code)
spot.code = code
end
end
-- Move spot to a new location
function Olympus.moveSpot(spotID, lat, lng)
Olympus.debug("Olympus.moveSpot " .. spotID .. " -> (" .. lat .. ", " .. lng .. ")", 2)
local spot = Olympus.spots[spotID]
if spot ~= nil then
spot.object:setPoint(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)))
spot.targetPosition = {lat = lat, lng = lng}
end
end
-- Remove the spot
function Olympus.deleteSpot(spotID)
Olympus.debug("Olympus.deleteSpot " .. spotID, 2)
local spot = Olympus.spots[spotID]
if spot ~= nil then
spot.object:destroy()
Olympus.spots[spotID]["active"] = false
end
end
-- Spawns a new unit or group
-- Spawn table contains the following parameters
-- category: (string), either Aircraft, Helicopter, GroundUnit or NavyUnit
-- coalition: (string)
-- country: (string)
-- airbaseName: (string, optional) only for air units
-- units: (array) Array of units to spawn. All units will be in the same group. Each unit element must contain:
-- unitType: (string) DCS Name of the unit
-- lat: (number)
-- lng: (number)
-- alt: (number, optional) only for air units
-- loadout: (string, optional) only for air units, must be one of the loadouts defined in unitPayloads.lua or mods.lua
-- payload: (table, optional) overrides loadout, specifies directly the loadout of the unit
-- liveryID: (string, optional)
function Olympus.spawnUnits(spawnTable, requestHash)
Olympus.debug("Olympus.spawnUnits " .. Olympus.serializeTable(spawnTable), 2)
local unitsTable = nil
local route = nil
local category = nil
-- Generate the units table and route as per DCS requirements
if spawnTable.category == 'Aircraft' then
unitsTable = Olympus.generateAirUnitsTable(spawnTable.units)
route = Olympus.generateAirUnitsRoute(spawnTable)
category = 'plane'
elseif spawnTable.category == 'Helicopter' then
unitsTable = Olympus.generateAirUnitsTable(spawnTable.units)
route = Olympus.generateAirUnitsRoute(spawnTable)
category = 'helicopter'
elseif spawnTable.category == 'GroundUnit' then
unitsTable = Olympus.generateGroundUnitsTable(spawnTable.units)
category = 'vehicle'
elseif spawnTable.category == 'NavyUnit' then
unitsTable = Olympus.generateNavyUnitsTable(spawnTable.units)
category = 'ship'
end
-- It the unit country is not specified, get a country that belongs to the coalition
local countryID = 0
if spawnTable.country == nil or spawnTable.country == "" then
countryID = Olympus.getCountryIDByCoalition(spawnTable.coalition)
else
countryID = country.id[spawnTable.country]
end
-- Save the units in the database, for cloning
for idx, unitTable in pairs(unitsTable) do
Olympus.addToDatabase(unitTable)
end
-- Spawn the new group
local vars =
{
units = unitsTable,
country = countryID,
category = category,
route = route,
name = "Olympus-" .. Olympus.unitCounter,
task = 'CAP'
}
Olympus.debug(Olympus.serializeTable(vars), 2)
local newGroup = mist.dynAdd(vars)
Olympus.unitCounter = Olympus.unitCounter + 1
Olympus.debug("Olympus.spawnUnits completed succesfully", 2)
if newGroup == nil then
Olympus.notify("Olympus.spawnUnits failed to spawn group: " .. Olympus.serializeTable(spawnTable), 30)
return nil
end
Olympus.executionResults[requestHash] = newGroup.groupId
end
-- Generates unit table for air units
function Olympus.generateAirUnitsTable(units)
local unitsTable = {}
for idx, unit in pairs(units) do
local loadout = unit.loadout -- loadout: a string, one of the names defined in unitPayloads.lua. Must be compatible with the unitType
local payload = unit.payload -- payload: a table, if present the unit will receive this specific payload. Overrides loadout
if unit.heading == nil then
unit.heading = 0
end
-- Define the loadout
if payload == nil then
if loadout ~= nil and loadout ~= "" and Olympus.unitPayloads[unit.unitType] and Olympus.unitPayloads[unit.unitType][loadout] then
payload = { ["pylons"] = Olympus.unitPayloads[unit.unitType][loadout], ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 }
elseif loadout ~= nil and loadout ~= "" and Olympus.modsUnitPayloads ~= nil and Olympus.modsUnitPayloads[unit.unitType] and Olympus.modsUnitPayloads[unit.unitType][loadout] then
payload = { ["pylons"] = Olympus.modsUnitPayloads[unit.unitType][loadout], ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 }
else
payload = { ["pylons"] = {}, ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 }
end
end
-- Generate the unit table
local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0))
unitsTable[#unitsTable + 1] =
{
["type"] = unit.unitType,
["x"] = spawnLocation.x,
["y"] = spawnLocation.z,
["alt"] = unit.alt,
["alt_type"] = "BARO",
["skill"] = unit.skill,
["payload"] = payload,
["heading"] = unit.heading,
["callsign"] = { [1] = 1, [2] = 1, [3] = 1, ["name"] = "Olympus" .. Olympus.unitCounter.. "-" .. #unitsTable + 1 },
["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1,
["livery_id"] = unit.liveryID
}
end
return unitsTable
end
function Olympus.generateAirUnitsRoute(spawnTable)
local airbaseName = spawnTable.airbaseName -- airbaseName: a string, if present the aircraft will spawn on the ground of the selected airbase
local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(spawnTable.units[1].lat, spawnTable.units[1].lng, 0))
-- If a airbase is provided the first waypoint is set as a From runway takeoff.
local route = {}
if airbaseName and airbaseName ~= "" then
local airbase = Airbase.getByName(airbaseName)
if airbase then
local airbaseID = airbase:getID()
route =
{
["points"] =
{
[1] =
{
["action"] = "From Parking Area Hot",
["tasks"] = {
[1] = {["number"] = 1, ["auto"] = true, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = {["action"] = {["id"] = "EPLRS", ["params"] = {["value"] = true}, }, }, },
[2] = {["number"] = 2, ["auto"] = false, ["id"] = "Orbit", ["enabled"] = true, ["params"] = {["pattern"] = "Circle"}, },
},
["type"] = "TakeOffParkingHot",
["ETA"] = 0,
["ETA_locked"] = true,
["x"] = spawnLocation.x,
["y"] = spawnLocation.z,
["alt_type"] = "BARO",
["formation_template"] = "",
["airdromeId"] = airbaseID,
["speed_locked"] = true,
},
},
}
end
else
route = {
["points"] =
{
[1] =
{
["alt"] = alt,
["alt_type"] = "BARO",
["tasks"] = {
[1] = {["number"] = 1, ["auto"] = true, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = {["action"] = {["id"] = "EPLRS", ["params"] = {["value"] = true}, }, }, },
[2] = {["number"] = 2, ["auto"] = false, ["id"] = "Orbit", ["enabled"] = true, ["params"] = {["pattern"] = "Circle"}, },
},
["type"] = "Turning Point",
["x"] = spawnLocation.x,
["y"] = spawnLocation.z,
},
},
}
end
return route
end
-- Generates ground units table, either single or from template
function Olympus.generateGroundUnitsTable(units)
local unitsTable = {}
for idx, unit in pairs(units) do
local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0))
if Olympus.hasKey(templates, unit.unitType) then
for idx, value in pairs(templates[unit.unitType].units) do
unitsTable[#unitsTable + 1] =
{
["type"] = value.name,
["x"] = spawnLocation.x + value.dx,
["y"] = spawnLocation.z + value.dy,
["heading"] = 0,
["skill"] = unit.skill,
["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1
}
end
else
unitsTable[#unitsTable + 1] =
{
["type"] = unit.unitType,
["x"] = spawnLocation.x,
["y"] = spawnLocation.z,
["heading"] = unit.heading,
["skill"] = unit.skill,
["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1,
["livery_id"] = unit.liveryID
}
end
end
return unitsTable
end
-- Generates navy units table, either single or from template
function Olympus.generateNavyUnitsTable(units)
local unitsTable = {}
for idx, unit in pairs(units) do
local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0))
if Olympus.hasKey(templates, unit.unitType) then
for idx, value in pairs(templates[unit.unitType].units) do
unitsTable[#unitsTable + 1] =
{
["type"] = value.name,
["x"] = spawnLocation.x + value.dx,
["y"] = spawnLocation.z + value.dy,
["heading"] = 0,
["skill"] = unit.skill,
["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1,
["transportable"] = { ["randomTransportable"] = false }
}
end
else
unitsTable[#unitsTable + 1] =
{
["type"] = unit.unitType,
["x"] = spawnLocation.x,
["y"] = spawnLocation.z,
["heading"] = unit.heading,
["skill"] = unit.skill,
["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1,
["transportable"] = { ["randomTransportable"] = false },
["livery_id"] = unit.liveryID
}
end
end
return unitsTable
end
-- Add the unit data to the database, used for unit cloning
function Olympus.addToDatabase(unitTable)
Olympus.cloneDatabase[unitTable.name] = unitTable
end
-- Find a database entry by ID
function Olympus.findInDatabase(ID)
for idx, unitRecord in pairs(Olympus.cloneDatabase) do
if unitRecord ~= nil and unitRecord["ID"] == ID then
return unitRecord
end
end
return nil
end
-- Clones a unit by ID. Will clone the unit with the same original payload as the source unit. TODO: only works on Olympus unit not ME units (TO BE VERIFIED).
-- cloneTable is an array of element, each of which contains
-- ID: (number) ID of the unit to clone
-- lat: (number)
-- lng: (number)
function Olympus.clone(cloneTable, deleteOriginal)
Olympus.debug("Olympus.clone " .. Olympus.serializeTable(cloneTable), 2)
local unitsTable = {}
local countryID = nil
local category = nil
local route = {}
-- All the units in the table will be cloned in a single group
for idx, cloneData in pairs(cloneTable) do
local ID = cloneData.ID
local unitRecord = Olympus.findInDatabase(ID)
if unitRecord ~= nil then
-- Update the data of the cloned unit
local unitTable = mist.utils.deepCopy(unitRecord)
local point = coord.LLtoLO(cloneData['lat'], cloneData['lng'], 0)
if unitTable then
unitTable["x"] = point.x
unitTable["y"] = point.z
unitTable["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1
end
if countryID == nil and category == nil then
countryID = unitRecord["country"]
if unitRecord["category"] == Unit.Category.AIRPLANE then
category = 'plane'
route = {
["points"] =
{
[1] =
{
["alt"] = alt,
["alt_type"] = "BARO",
["tasks"] = {
[1] = {["number"] = 1, ["auto"] = true, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = {["action"] = {["id"] = "EPLRS", ["params"] = {["value"] = true}, }, }, },
[2] = {["number"] = 2, ["auto"] = false, ["id"] = "Orbit", ["enabled"] = true, ["params"] = {["pattern"] = "Circle"}, },
},
["type"] = "Turning Point",
["x"] = point.x,
["y"] = point.z,
},
},
}
elseif unitRecord["category"] == Unit.Category.HELICOPTER then
category = 'helicopter'
route = {
["points"] =
{
[1] =
{
["alt"] = alt,
["alt_type"] = "BARO",
["tasks"] = {
[1] = {["number"] = 1, ["auto"] = true, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = {["action"] = {["id"] = "EPLRS", ["params"] = {["value"] = true}, }, }, },
[2] = {["number"] = 2, ["auto"] = false, ["id"] = "Orbit", ["enabled"] = true, ["params"] = {["pattern"] = "Circle"}, },
},
["type"] = "Turning Point",
["x"] = point.x,
["y"] = point.z,
},
},
}
elseif unitRecord["category"] == Unit.Category.GROUND_UNIT then
category = 'vehicle'
elseif unitRecord["category"] == Unit.Category.SHIP then
category = 'ship'
end
end
unitsTable[#unitsTable + 1] = mist.utils.deepCopy(unitTable)
end
if deleteOriginal then
Olympus.delete(ID, false)
end
end
local vars =
{
units = unitsTable,
country = countryID,
category = category,
route = route,
name = "Olympus-" .. Olympus.unitCounter,
task = 'CAP'
}
Olympus.debug(Olympus.serializeTable(vars), 1)
-- Save the units in the database, for cloning
for idx, unitTable in pairs(unitsTable) do
Olympus.addToDatabase(unitTable)
end
Olympus.debug(Olympus.serializeTable(vars), 2)
mist.dynAdd(vars)
Olympus.unitCounter = Olympus.unitCounter + 1
Olympus.debug("Olympus.clone completed successfully", 2)
end
-- Delete a unit by ID, optionally use an explosion
function Olympus.delete(ID, explosion, explosionType)
Olympus.debug("Olympus.delete " .. ID .. " " .. tostring(explosion), 2)
local unit = Olympus.getUnitByID(ID)
if unit ~= nil and unit:isExist() then
if unit:getPlayerName() or explosion then
if explosionType == nil then
explosionType = "normal"
end
local lat, lng, alt = coord.LOtoLL(unit:getPoint())
Olympus.explosion(250, explosionType, lat, lng, alt)
Olympus.debug("Olympus.delete completed successfully", 2)
else
unit:destroy(); --works for AI units not players
Olympus.debug("Olympus.delete completed successfully", 2)
end
end
end
-- Set a DCS main task to a group
function Olympus.setTask(groupName, taskOptions)
Olympus.debug("Olympus.setTask " .. groupName .. " " .. Olympus.serializeTable(taskOptions), 2)
local group = Group.getByName(groupName)
if group ~= nil then
local task = Olympus.buildTask(groupName, taskOptions);
Olympus.debug("Olympus.setTask " .. Olympus.serializeTable(task), 20)
if task then
group:getController():setTask(task)
Olympus.debug("Olympus.setTask completed successfully", 2)
end
end
end
-- Reset the task of a group
function Olympus.resetTask(groupName)
Olympus.debug("Olympus.resetTask " .. groupName, 2)
local group = Group.getByName(groupName)
if group ~= nil then
group:getController():resetTask()
Olympus.debug("Olympus.resetTask completed successfully", 2)
end
end
-- Give a group a command
function Olympus.setCommand(groupName, command)
Olympus.debug("Olympus.setCommand " .. groupName .. " " .. Olympus.serializeTable(command), 2)
local group = Group.getByName(groupName)
if group ~= nil then
group:getController():setCommand(command)
Olympus.debug("Olympus.setCommand completed successfully", 2)
end
end
-- Set an option of a group
function Olympus.setOption(groupName, optionID, optionValue)
Olympus.debug("Olympus.setOption " .. groupName .. " " .. optionID .. " " .. tostring(optionValue), 2)
local group = Group.getByName(groupName)
if group ~= nil then
group:getController():setOption(optionID, optionValue)
Olympus.debug("Olympus.setOption completed successfully", 2)
end
end
-- Disable the AI of a group on or off entirely
function Olympus.setOnOff(groupName, onOff)
Olympus.debug("Olympus.setOnOff " .. groupName .. " " .. tostring(onOff), 2)
local group = Group.getByName(groupName)
if group ~= nil then
group:getController():setOnOff(onOff)
Olympus.debug("Olympus.setOnOff completed successfully", 2)
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)
return str:match("^%[(.-)%]")
end
local navpoints = {}
if mist.DBs.navPoints ~= nil then
for coalitionName, coalitionNavpoints in pairs(mist.DBs.navPoints) do
if navpoints[coalitionName] == nil then
navpoints[coalitionName] = {}
end
for index, navpointDrawingData in pairs(coalitionNavpoints) do
local navpointCustomLayer = extract_tag(navpointDrawingData['callsignStr']);
-- Let's convert DCS coords to lat lon
local vec3 = { x = navpointDrawingData['x'], y = 0, z = navpointDrawingData['y'] }
local lat, lng = coord.LOtoLL(vec3)
navpointDrawingData['lat'] = lat
navpointDrawingData['lng'] = lng
navpointDrawingData['coalition'] = coalitionName
navpointDrawingData['tag'] = navpointCustomLayer
if navpointCustomLayer ~= nil then
if navpoints[coalitionName][navpointCustomLayer] == nil then
navpoints[coalitionName][navpointCustomLayer] = {}
end
navpoints[coalitionName][navpointCustomLayer][navpointDrawingData['callsignStr']] = navpointDrawingData
else
navpoints[coalitionName][navpointDrawingData['callsignStr']] = navpointDrawingData
end
end
end
end
return navpoints
end
-- This function is periodically called to collect the data of all the existing drawings in the mission to be transmitted to the olympus.dll
function Olympus.initializeDrawings()
local function extract_custom_layer_name(str)
if str:match("^%[LYR:(.-)%]") then
return str:match("^%[LYR:(.-)%]")
elseif str:match("^%[(.-)%]") then
return str:match("^%[(.-)%]")
end
return nil
end
local drawings = {}
if mist.DBs.drawingByName ~= nil then
for drawingName, drawingData in pairs(mist.DBs.drawingByName) do
local customLayer = extract_custom_layer_name(drawingName)
-- Let's convert DCS coords to lat lon
local vec3 = { x = drawingData['mapX'], y = 0, z = drawingData['mapY'] }
local lat, lng = coord.LOtoLL(vec3)
drawingData['lat'] = lat
drawingData['lng'] = lng
-- If the drawing has points, we have to convert those too
if drawingData['points'] ~= nil then
if drawingData['points']['x'] ~= nil then
-- In this case we have only one point
local point = { x = drawingData['points']['x'], y = 0, z = drawingData['points']['y'] }
local pointLat, pointLng = coord.LOtoLL(point)
drawingData['points'][0] = { lat = pointLat, lng = pointLng }
else
-- In this case we have multiple points indexed by number
for pointNumber, pointLOCoords in pairs(drawingData['points']) do
local point = { x = pointLOCoords['x'], y = 0, z = pointLOCoords['y'] }
local pointLat, pointLng = coord.LOtoLL(point)
drawingData['points'][pointNumber] = { lat = pointLat, lng = pointLng }
end
end
end
-- Let's initialize the layers
if drawings[drawingData.layerName] == nil then
drawings[drawingData.layerName] = {}
end
if customLayer and drawings[drawingData.layerName][customLayer] == nil then
drawings[drawingData.layerName][customLayer] = {}
end
-- Let's put the drawing in the correct layer
if customLayer then
-- Let's remove the m from the drawing name
local cleanDrawingName = string.match(drawingName, "%] (.+)")
drawingData.name = cleanDrawingName
-- The drawing has the custom layer m
drawings[drawingData.layerName][customLayer][cleanDrawingName] = drawingData
else
-- The drawing is a standard drawing
drawings[drawingData.layerName][drawingName] = drawingData
end
end
local navpoints = Olympus.getNavPoints()
drawings['navpoints'] = navpoints
Olympus.drawingsByLayer["drawings"] = drawings
-- Send the drawings to the DLL
Olympus.OlympusDLL.setDrawingsData()
Olympus.notify("Olympus drawings initialized", 2)
else
Olympus.debug("MIST DBs not ready", 2)
timer.scheduleFunction(Olympus.initializeDrawings, {}, timer.getTime() + 1)
end
end
-- This function is periodically called to collect the data of all the existing units in the mission to be transmitted to the olympus.dll
function Olympus.setUnitsData(arg, time)
-- Units data
local units = {}
local startIndex = Olympus.unitIndex
local endIndex = startIndex + Olympus.unitStep
local index = 0
for ID, unit in pairs(Olympus.units) do
index = index + 1
-- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles
if index > startIndex then
if unit ~= nil and unit:isExist() then
local table = {}
-- Get the object category in Olympus name
local objectCategory = Object.getCategory(unit)
if objectCategory == Object.Category.UNIT then
if unit:getDesc().category == Unit.Category.AIRPLANE then
table["category"] = "Aircraft"
elseif unit:getDesc().category == Unit.Category.HELICOPTER then
table["category"] = "Helicopter"
elseif unit:getDesc().category == Unit.Category.GROUND_UNIT then
table["category"] = "GroundUnit"
elseif unit:getDesc().category == Unit.Category.SHIP then
table["category"] = "NavyUnit"
elseif Olympus.modsList ~= nil and Olympus.modsList[unit:getDesc().typeName] ~= nil then
table["category"] = Olympus.modsList[unit:getDesc().typeName]
end
else
local status, description = pcall(getUnitDescription, unit)
if status and Olympus.modsList ~= nil and Olympus.modsList[description.typeName] ~= nil then
table["category"] = Olympus.modsList[description.typeName]
else
units[ID] = {isAlive = false}
Olympus.units[ID] = nil
end
end
-- If the category is handled by Olympus, get the data
if table["category"] ~= nil then
-- Compute unit position and heading
local lat, lng, alt = coord.LOtoLL(unit:getPoint())
local position = unit:getPosition()
local heading = math.atan2( position.x.z, position.x.x )
local velocity = unit:getVelocity();
local airborne = unit:inAir()
-- Fill the data table
table["unitID"] = unit:getID()
table["name"] = unit:getTypeName()
table["coalitionID"] = unit:getCoalition()
table["position"] = {}
table["position"]["lat"] = lat
table["position"]["lng"] = lng
table["position"]["alt"] = alt
table["speed"] = mist.vec.mag(velocity)
table["horizontalVelocity"] = math.sqrt(velocity.x * velocity.x + velocity.z * velocity.z)
table["verticalVelocity"] = velocity.y
table["heading"] = heading
table["airborne"] = airborne
-- Track angles are wrong because of weird reference systems, approximate it using latitude and longitude differences
if (table["horizontalVelocity"] > 1) then
if Olympus.unitsData["units"] ~= nil and Olympus.unitsData["units"][ID] ~= nil and Olympus.unitsData["units"][ID]["position"] ~= nil and Olympus.unitsData["units"][ID]["position"]["lat"] ~= nil and Olympus.unitsData["units"][ID]["position"]["lng"] ~= nil then
local latDifference = lat - Olympus.unitsData["units"][ID]["position"]["lat"]
local lngDifference = lng - Olympus.unitsData["units"][ID]["position"]["lng"]
table["track"] = math.atan2(lngDifference * math.cos(lat / 57.29577), latDifference)
else
table["track"] = math.atan2(velocity.z, velocity.x)
end
else
table["track"] = table["heading"]
end
table["isAlive"] = unit:isExist() and unit:isActive() and unit:getLife() >= 1
--[[ COMMENTING OUT BECAUSE OF CRASHES -- TO BE INVESTIGATED LATER ON ]]--
--[[ if unit:isActive() and unit:hasSensors(Unit.SensorType.RADAR) then
if unit:getRadar() then
table["radarState"] = true
else
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
local controller = group:getController()
if controller ~= nil then
-- Get the targets detected by the unit controller
local contacts = {}
local unitController = unit:getController()
if unitController ~= nil then
for det, enum in pairs(Controller.Detection) do
local controllerTargets = unitController:getDetectedTargets(enum)
for i, target in ipairs(controllerTargets) do
if target ~= nil and target.object ~= nil and target.visible then
target["detectionMethod"] = det
contacts[#contacts + 1] = target
end
end
end
end
-- getLife0 does not seem to work for ships, so we need to keep a reference to the initial life of the unit
if Olympus.unitsInitialLife[ID] == nil then
Olympus.unitsInitialLife[ID] = unit:getLife()
end
-- Get the initial life of the unit to compute the current health
local initialLife = 1
if Olympus.unitsInitialLife[ID] ~= nil then
initialLife = Olympus.unitsInitialLife[ID]
end
table["country"] = unit:getCountry()
if unit:getPlayerName() ~= nil then
table["unitName"] = unit:getPlayerName()
else
table["unitName"] = unit:getName()
end
-- In case of AI units the callSign and the unitName will be the same
table["callsign"] = unit:getName()
table["groupID"] = group:getID()
table["groupName"] = group:getName()
table["isHuman"] = (unit:getPlayerName() ~= nil)
table["hasTask"] = controller:hasTask()
table["ammo"] = unit:getAmmo() --TODO remove a lot of stuff we don't really need
table["fuel"] = unit:getFuel()
table["health"] = unit:getLife() / initialLife * 100
table["contacts"] = contacts
local name = unit:getName()
-- If the unit is not in the clone database it means it was not spawned by Olympus. Let's try and recover it using mist
if Olympus.cloneDatabase[name] == nil then
if mist.DBs ~= nil and mist.DBs.unitsByName ~= nil and mist.DBs.unitsByName[name] ~= nil then
-- Payloads can be copied from ME units only TODO: can we fix this?
local payload = {}
if mist.DBs.MEunitsByName[name] then
payload = mist.getPayload(name)
end
-- Create a mock spawn table to generate the database
local unitsTable = nil
local spawnTable = {}
spawnTable.units = {
[1] = {
["unitType"] = table["name"],
["lat"] = table["position"]["lat"],
["lng"] = table["position"]["lng"],
["alt"] = table["position"]["alt"],
["payload"] = payload,
["liveryID"] = mist.DBs.unitsByName[name]["livery_id"]
}
}
-- Generate the units table as per DCS requirements
if table["category"] == 'Aircraft' then
unitsTable = Olympus.generateAirUnitsTable(spawnTable.units)
elseif table["category"] == 'Helicopter' then
unitsTable = Olympus.generateAirUnitsTable(spawnTable.units)
elseif table["category"] == 'GroundUnit' then
unitsTable = Olympus.generateGroundUnitsTable(spawnTable.units)
elseif table["category"] == 'NavyUnit' then
unitsTable = Olympus.generateNavyUnitsTable(spawnTable.units)
end
-- Save the units in the database, for cloning
for idx, unitTable in pairs(unitsTable) do
-- Force the name of the unit to be equal to the original name
unitTable["name"] = name
Olympus.addToDatabase(mist.utils.deepCopy(unitTable))
end
end
end
-- Update the database used for unit cloning
if Olympus.cloneDatabase[name] ~= nil then
Olympus.cloneDatabase[name]["ID"] = ID
Olympus.cloneDatabase[name]["category"] = unit:getDesc().category
Olympus.cloneDatabase[name]["heading"] = table["heading"]
Olympus.cloneDatabase[name]["alt"] = alt
Olympus.cloneDatabase[name]["country"] = unit:getCountry()
end
units[ID] = table
end
else
-- If the unit reference is nil it means the unit no longer exits
units[ID] = {isAlive = false}
Olympus.units[ID] = nil
end
end
else
-- If the unit reference is nil it means the unit no longer exits
units[ID] = {isAlive = false}
Olympus.units[ID] = nil
end
end
if index >= endIndex then
break
end
end
-- Reset the counter
if index ~= endIndex then
Olympus.unitIndex = 0
else
Olympus.unitIndex = endIndex
end
-- Assemble unitsData table
Olympus.unitsData["units"] = units
Olympus.OlympusDLL.setUnitsData()
return time + 0.05
end
-- This function is periodically called to collect the data of all the existing weapons in the mission to be transmitted to the olympus.dll
function Olympus.setWeaponsData(arg, time)
-- Weapons data
local weapons = {}
local startIndex = Olympus.weaponIndex
local endIndex = startIndex + Olympus.weaponStep
local index = 0
for ID, weapon in pairs(Olympus.weapons) do
index = index + 1
-- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles
if index > startIndex then
if weapon ~= nil and weapon:isExist() then
local table = {}
-- Get the object category in Olympus name
local objectCategory = Object.getCategory(weapon)
if objectCategory == Object.Category.WEAPON then
if weapon:getDesc().category == Weapon.Category.MISSILE then
table["category"] = "Missile"
elseif weapon:getDesc().category == Weapon.Category.ROCKET then
table["category"] = "Missile"
elseif weapon:getDesc().category == Weapon.Category.BOMB then
table["category"] = "Bomb"
--elseif weapon:getDesc().category == Weapon.Category.SHELL then
-- table["category"] = "Shell" -- Useful for debugging but has no real use and has big impact on performance
end
else
weapons[ID] = {isAlive = false}
Olympus.weapons[ID] = nil
end
-- If the category is handled by Olympus, get the data
if table["category"] ~= nil then
-- Compute weapon position and heading
local lat, lng, alt = coord.LOtoLL(weapon:getPoint())
local position = weapon:getPosition()
local heading = math.atan2( position.x.z, position.x.x )
-- Fill the data table
table["name"] = weapon:getTypeName()
table["coalitionID"] = weapon:getCoalition()
table["position"] = {}
table["position"]["lat"] = lat
table["position"]["lng"] = lng
table["position"]["alt"] = alt
table["speed"] = mist.vec.mag(weapon:getVelocity())
table["heading"] = heading
table["isAlive"] = weapon:isExist()
weapons[ID] = table
end
else
-- If the weapon reference is nil it means the unit no longer exits
weapons[ID] = {isAlive = false}
Olympus.weapons[ID] = nil
end
end
if index >= endIndex then
break
end
end
-- Reset the counter
if index ~= endIndex then
Olympus.weaponIndex = 0
else
Olympus.weaponIndex = endIndex
end
-- Assemble weaponsData table
Olympus.weaponsData["weapons"] = weapons
Olympus.OlympusDLL.setWeaponsData()
return time + 0.25
end
function Olympus.setExecutionResults()
Olympus.OlympusDLL.setExecutionResults()
return timer.getTime() + 1
end
function Olympus.setMissionData(arg, time)
-- Bullseye data
local bullseyes = {}
for i = 0, 2 do
local bullseyeVec3 = coalition.getMainRefPoint(i)
local bullseyeLatitude, bullseyeLongitude, bullseyeAltitude = coord.LOtoLL(bullseyeVec3)
bullseyes[i] = {}
bullseyes[i]["latitude"] = bullseyeLatitude
bullseyes[i]["longitude"] = bullseyeLongitude
bullseyes[i]["coalition"] = Olympus.getCoalitionByCoalitionID(i)
end
-- Airbases data
local base = world.getAirbases()
local airbases = {}
for i = 1, #base do
local info = {}
local latitude, longitude, altitude = coord.LOtoLL(Airbase.getPoint(base[i]))
info["callsign"] = Airbase.getCallsign(base[i])
info["coalition"] = Olympus.getCoalitionByCoalitionID(Airbase.getCoalition(base[i]))
info["latitude"] = latitude
info["longitude"] = longitude
if Airbase.getUnit(base[i]) then
info["unitId"] = Airbase.getUnit(base[i]):getID()
end
airbases[i] = info
end
-- Mission
local mission = {}
mission.theatre = env.mission.theatre
mission.dateAndTime = {
["elapsedTime"] = DCS.getRealTime() - Olympus.missionStartTime,
["time"] = mist.time.getDHMS(timer.getAbsTime()),
["startTime"] = env.mission.start_time,
["date"] = env.mission.date
}
mission.coalitions = {
["red"] = {},
["blue"] = {},
["neutral"] = {}
}
for countryName, countryId in pairs(country["id"]) do
local coalitionName = Olympus.getCoalitionByCoalitionID(coalition.getCountryCoalition(countryId))
mission.coalitions[coalitionName][#mission.coalitions[coalitionName] + 1] = countryName
end
-- Spots
-- Initialize an empty table to store spots
local spots = {}
-- Iterate over each spot in Olympus.spots
for ID, spot in pairs(Olympus.spots) do
-- Create a new entry in the spots table with the same ID
spots[ID] = {
type = spot.type,
sourceUnitID = spot.sourceUnitID,
targetPosition = spot.targetPosition,
active = spot.active,
}
-- If the spot type is "laser", add the code to the spot entry
if spot.type == "laser" then
spots[ID]["code"] = spot.code
end
end
-- Assemble table
Olympus.missionData["bullseyes"] = bullseyes
Olympus.missionData["airbases"] = airbases
Olympus.missionData["mission"] = mission
Olympus.missionData["spots"] = spots
Olympus.OlympusDLL.setMissionData()
return time + 1 -- For perfomance reasons mission data is updated once every second
end
-- Initializes the units table with all the existing ME units
function Olympus.initializeUnits()
if mist and mist.DBs and mist.DBs.MEunitsById then
for id, unitsTable in pairs(mist.DBs.MEunitsById) do
local unit = Unit.getByName(unitsTable["unitName"])
if unit ~= nil and unit:isExist() then
Olympus.units[unit["id_"]] = unit
end
end
Olympus.notify("Olympus units table initialized", 2)
else
Olympus.debug("MIST DBs not ready", 2)
timer.scheduleFunction(Olympus.initializeUnits, {}, timer.getTime() + 1)
end
end
------------------------------------------------------------------------------------------------------
-- Olympus utility functions
------------------------------------------------------------------------------------------------------
function Olympus.serializeTable(val, name, skipnewlines, depth)
skipnewlines = skipnewlines or false
depth = depth or 0
local tmp = string.rep(" ", depth)
if name then
if type(name) == "number" then
tmp = tmp .. "[" .. name .. "]" .. " = "
else
tmp = tmp .. name .. " = "
end
end
if type(val) == "table" then
tmp = tmp .. "{" .. (not skipnewlines and "\n" or "")
for k, v in pairs(val) do
tmp = tmp .. Olympus.serializeTable(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "")
end
tmp = tmp .. string.rep(" ", depth) .. "}"
elseif type(val) == "number" then
tmp = tmp .. tostring(val)
elseif type(val) == "string" then
tmp = tmp .. string.format("%q", val)
elseif type(val) == "boolean" then
tmp = tmp .. (val and "true" or "false")
else
tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\""
end
return tmp
end
function Olympus.isArray(t)
local i = 0
for _ in pairs(t) do
i = i + 1
if t[i] == nil then return false end
end
return true
end
function Olympus.hasValue(tab, val)
for index, value in ipairs(tab) do
if value == val then
return true
end
end
return false
end
function Olympus.hasKey(tab, key)
for k, value in pairs(tab) do
if k == key then
return true
end
end
return false
end
------------------------------------------------------------------------------------------------------
-- Olympus startup script
------------------------------------------------------------------------------------------------------
Olympus.instancePath = lfs.writedir().."Mods\\Services\\Olympus\\bin\\"
Olympus.notify("Starting DCS Olympus backend session in "..Olympus.instancePath, 2)
local OlympusName = 'Olympus ' .. version .. ' C++ module';
Olympus.DLLsloaded = Olympus.loadDLLs()
if Olympus.DLLsloaded then
Olympus.notify(OlympusName..' successfully loaded.', 20)
else
Olympus.notify('Failed to load '..OlympusName, 20)
end
-- Create the handler to detect new units
if handler ~= nil then
world.removeEventHandler(handler)
Olympus.debug("Olympus handler removed" , 2)
end
handler = {}
function handler:onEvent(event)
if event.id == 1 then
local weapon = event.weapon
if Olympus ~= nil and Olympus.weapons ~= nil then
Olympus.weapons[weapon["id_"]] = weapon
Olympus.debug("New weapon created " .. weapon["id_"], 2)
end
elseif event.id == 15 then
local unit = event.initiator
if Olympus ~= nil and Olympus.units ~= nil then
Olympus.units[unit["id_"]] = unit
Olympus.debug("New unit created " .. unit["id_"], 2)
end
end
end
world.addEventHandler(handler)
-- Start the periodic functions
timer.scheduleFunction(Olympus.setUnitsData, {}, timer.getTime() + 0.05)
timer.scheduleFunction(Olympus.setWeaponsData, {}, timer.getTime() + 0.25)
timer.scheduleFunction(Olympus.setMissionData, {}, timer.getTime() + 1)
timer.scheduleFunction(Olympus.setExecutionResults, {}, timer.getTime() + 1)
-- Initialize the ME units
Olympus.initializeUnits()
-- Initialize the Drawings
Olympus.initializeDrawings()
Olympus.notify("OlympusCommand script " .. version .. " loaded successfully", 2, true)