mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
1706 lines
54 KiB
Lua
1706 lines
54 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 = {}
|
|
|
|
-- 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.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)
|
|
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)
|
|
mist.dynAdd(vars)
|
|
|
|
Olympus.unitCounter = Olympus.unitCounter + 1
|
|
Olympus.debug("Olympus.spawnUnits completed succesfully", 2)
|
|
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
|
|
|
|
function getUnitDescription(unit)
|
|
return unit:getDescr()
|
|
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["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
|
|
|
|
if unit:isActive() and unit:hasSensors(Unit.SensorType.RADAR) then
|
|
if unit:getRadar() then
|
|
table["radarState"] = true
|
|
else
|
|
table["radarState"] = false
|
|
end
|
|
end
|
|
|
|
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["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.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)
|
|
|
|
-- Initialize the ME units
|
|
Olympus.initializeUnits()
|
|
|
|
-- Initialize the Drawings
|
|
Olympus.initializeDrawings()
|
|
|
|
Olympus.notify("OlympusCommand script " .. version .. " loaded successfully", 2, true)
|
|
|