2024-07-18 18:56:59 +02:00

9533 lines
321 KiB
Lua

--[[--
MIST Mission Scripting Tools.
## Description:
MIssion Scripting Tools (MIST) is a collection of Lua functions
and databases that is intended to be a supplement to the standard
Lua functions included in the simulator scripting engine.
MIST functions and databases provide ready-made solutions to many common
scripting tasks and challenges, enabling easier scripting and saving
mission scripters time. The table mist.flagFuncs contains a set of
Lua functions (that are similar to Slmod functions) that do not
require detailed Lua knowledge to use.
However, the majority of MIST does require knowledge of the Lua language,
and, if you are going to utilize these components of MIST, it is necessary
that you read the Simulator Scripting Engine guide on the official ED wiki.
## Links:
ED Forum Thread: <http://forums.eagle.ru/showthread.php?t=98616>
##Github:
Development <https://github.com/mrSkortch/MissionScriptingTools>
Official Releases <https://github.com/mrSkortch/MissionScriptingTools/tree/master>
@script MIST
@author Speed
@author Grimes
@author lukrop
]]
mist = {}
-- don't change these
mist.majorVersion = 4
mist.minorVersion = 5
mist.build = 125
-- forward declaration of log shorthand
local log
local dbLog
local mistSettings = {
errorPopup = false, -- errors printed by mist logger will create popup warning you
warnPopup = false,
infoPopup = false,
logLevel = 'warn',
dbLog = 'warn',
}
do -- the main scope
local coroutines = {}
local tempSpawnedUnits = {} -- birth events added here
local tempSpawnedGroups = {}
local tempSpawnGroupsCounter = 0
local mistAddedObjects = {} -- mist.dynAdd unit data added here
local mistAddedGroups = {} -- mist.dynAdd groupdata added here
local writeGroups = {}
local lastUpdateTime = 0
local updateAliveUnitsCounter = 0
local updateTenthSecond = 0
local mistGpId = 70000
local mistUnitId = 70000
local mistDynAddIndex = {[' air '] = 0, [' hel '] = 0, [' gnd '] = 0, [' bld '] = 0, [' static '] = 0, [' shp '] = 0}
local scheduledTasks = {}
local taskId = 0
local idNum = 0
mist.nextGroupId = 1
mist.nextUnitId = 1
local function initDBs() -- mist.DBs scope
mist.DBs = {}
mist.DBs.markList = {}
mist.DBs.missionData = {}
if env.mission then
mist.DBs.missionData.startTime = env.mission.start_time
mist.DBs.missionData.theatre = env.mission.theatre
mist.DBs.missionData.version = env.mission.version
mist.DBs.missionData.files = {}
if type(env.mission.resourceCounter) == 'table' then
for fIndex, fData in pairs (env.mission.resourceCounter) do
mist.DBs.missionData.files[#mist.DBs.missionData.files + 1] = mist.utils.deepCopy(fIndex)
end
end
-- if we add more coalition specific data then bullseye should be categorized by coaliton. For now its just the bullseye table
mist.DBs.missionData.bullseye = {}
mist.DBs.missionData.countries = {}
end
mist.DBs.drawingByName = {}
mist.DBs.drawingIndexed = {}
if env.mission.drawings and env.mission.drawings.layers then
for i = 1, #env.mission.drawings.layers do
local l = env.mission.drawings.layers[i]
for j = 1, #l.objects do
local copy = mist.utils.deepCopy(l.objects[j])
--log:warn(copy)
local doOffset = false
copy.layer = l.name
local theta = copy.angle or 0
theta = math.rad(theta)
if copy.primitiveType == "Polygon" then
if copy.polygonMode == 'rect' then
local h, w = copy.height, copy.width
copy.points = {}
copy.points[1] = {x = h/2, y = w/2}
copy.points[2] = {x = -h/2, y = w/2}
copy.points[3] = {x = -h/2, y = -w/2}
copy.points[4] = {x = h/2, y = -w/2}
doOffset = true
elseif copy.polygonMode == "circle" then
copy.points = {x = copy.mapX, y = copy.mapY}
elseif copy.polygonMode == 'oval' then
copy.points = {}
local numPoints = 24
local angleStep = (math.pi*2)/numPoints
doOffset = true
for v = 1, numPoints do
local pointAngle = v * angleStep
local x = copy.r1 * math.cos(pointAngle)
local y = copy.r2 * math.sin(pointAngle)
table.insert(copy.points,{x=x,y=y})
end
elseif copy.polygonMode == "arrow" then
doOffset = true
end
if theta ~= 0 and copy.points and doOffset == true then
--log:warn('offsetting Values')
for p = 1, #copy.points do
local offset = mist.vec.rotateVec2(copy.points[p], theta)
copy.points[p] = offset
end
--log:warn(copy.points[1])
end
elseif copy.primitiveType == "Line" and copy.closed == true then
table.insert(copy.points, mist.utils.deepCopy(copy.points[1]))
end
if copy.points and #copy.points > 1 then
for u = 1, #copy.points do
copy.points[u].x = mist.utils.round(copy.points[u].x + copy.mapX, 2)
copy.points[u].y = mist.utils.round(copy.points[u].y + copy.mapY, 2)
end
end
if mist.DBs.drawingByName[copy.name] then
log:warn("Drawing by the name of [ $1 ] already exists in DB. Failed to add to mist.DBs.drawingByName.", copy.name)
else
mist.DBs.drawingByName[copy.name] = copy
end
table.insert(mist.DBs.drawingIndexed, copy)
end
end
end
local abRef = {units = {}, airbase = {}}
for ind, val in pairs(world.getAirbases()) do
local cat = "airbase"
if Airbase.getDesc(val).category > 0 then
cat = "units"
end
abRef[cat][tonumber(val:getID())] = {name = val:getName()}
end
mist.DBs.navPoints = {}
mist.DBs.units = {}
--Build mist.db.units and mist.DBs.navPoints
for coa_name_miz, coa_data in pairs(env.mission.coalition) do
local coa_name = coa_name_miz
if string.lower(coa_name_miz) == 'neutrals' then
coa_name = 'neutral'
end
local coaEnum = coalition.side[string.upper(coa_name)]
if type(coa_data) == 'table' then
mist.DBs.units[coa_name] = {}
if coa_data.bullseye then
mist.DBs.missionData.bullseye[coa_name] = {}
mist.DBs.missionData.bullseye[coa_name].x = coa_data.bullseye.x
mist.DBs.missionData.bullseye[coa_name].y = coa_data.bullseye.y
end
-- build nav points DB
mist.DBs.navPoints[coa_name] = {}
if coa_data.nav_points then --navpoints
--mist.debug.writeData (mist.utils.serialize,{'NavPoints',coa_data.nav_points}, 'NavPoints.txt')
for nav_ind, nav_data in pairs(coa_data.nav_points) do
if type(nav_data) == 'table' then
mist.DBs.navPoints[coa_name][nav_ind] = mist.utils.deepCopy(nav_data)
mist.DBs.navPoints[coa_name][nav_ind].name = nav_data.callsignStr -- name is a little bit more self-explanatory.
mist.DBs.navPoints[coa_name][nav_ind].point = {} -- point is used by SSE, support it.
mist.DBs.navPoints[coa_name][nav_ind].point.x = nav_data.x
mist.DBs.navPoints[coa_name][nav_ind].point.y = 0
mist.DBs.navPoints[coa_name][nav_ind].point.z = nav_data.y
end
end
end
if coa_data.country then --there is a country table
for cntry_id, cntry_data in pairs(coa_data.country) do
local countryName = string.lower(cntry_data.name)
if cntry_data.id and country.names[cntry_data.id] then
countryName = string.lower(country.names[cntry_data.id])
end
mist.DBs.missionData.countries[countryName] = coa_name
mist.DBs.units[coa_name][countryName] = {}
mist.DBs.units[coa_name][countryName].countryId = cntry_data.id
if type(cntry_data) == 'table' then --just making sure
for obj_cat_name, obj_cat_data in pairs(cntry_data) do
if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then --should be an unncessary check
local category = obj_cat_name
if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group!
mist.DBs.units[coa_name][countryName][category] = {}
for group_num, group_data in pairs(obj_cat_data.group) do
local helipadId
local airdromeId
if group_data.route and group_data.route.points and group_data.route.points[1] then
if group_data.route.points[1].airdromeId then
airdromeId = group_data.route.points[1].airdromeId
--table.insert(abRef.airbase[group_data.route.points[1].airdromeId], group_data.groupId)
elseif group_data.route.points[1].helipadId then
helipadId = group_data.route.points[1].helipadId
--table.insert(abRef.units[group_data.route.points[1].helipadId], group_data.groupId)
end
end
if group_data and group_data.units and type(group_data.units) == 'table' then --making sure again- this is a valid group
mist.DBs.units[coa_name][countryName][category][group_num] = {}
local groupName = group_data.name
if env.mission.version > 7 and env.mission.version < 19 then
groupName = env.getValueDictByKey(groupName)
end
mist.DBs.units[coa_name][countryName][category][group_num].groupName = groupName
mist.DBs.units[coa_name][countryName][category][group_num].groupId = group_data.groupId
mist.DBs.units[coa_name][countryName][category][group_num].category = category
mist.DBs.units[coa_name][countryName][category][group_num].coalition = coa_name
mist.DBs.units[coa_name][countryName][category][group_num].coalitionId = coaEnum
mist.DBs.units[coa_name][countryName][category][group_num].country = countryName
mist.DBs.units[coa_name][countryName][category][group_num].countryId = cntry_data.id
mist.DBs.units[coa_name][countryName][category][group_num].startTime = group_data.start_time
mist.DBs.units[coa_name][countryName][category][group_num].task = group_data.task
mist.DBs.units[coa_name][countryName][category][group_num].hidden = group_data.hidden
mist.DBs.units[coa_name][countryName][category][group_num].units = {}
mist.DBs.units[coa_name][countryName][category][group_num].radioSet = group_data.radioSet
mist.DBs.units[coa_name][countryName][category][group_num].uncontrolled = group_data.uncontrolled
mist.DBs.units[coa_name][countryName][category][group_num].frequency = group_data.frequency
mist.DBs.units[coa_name][countryName][category][group_num].modulation = group_data.modulation
for unit_num, unit_data in pairs(group_data.units) do
local units_tbl = mist.DBs.units[coa_name][countryName][category][group_num].units --pointer to the units table for this group
units_tbl[unit_num] = {}
if env.mission.version > 7 and env.mission.version < 19 then
units_tbl[unit_num].unitName = env.getValueDictByKey(unit_data.name)
else
units_tbl[unit_num].unitName = unit_data.name
end
units_tbl[unit_num].type = unit_data.type
units_tbl[unit_num].skill = unit_data.skill --will be nil for statics
units_tbl[unit_num].unitId = unit_data.unitId
units_tbl[unit_num].category = category
units_tbl[unit_num].coalition = coa_name
units_tbl[unit_num].coalitionId = coaEnum
units_tbl[unit_num].country = countryName
units_tbl[unit_num].countryId = cntry_data.id
units_tbl[unit_num].heading = unit_data.heading
units_tbl[unit_num].playerCanDrive = unit_data.playerCanDrive
units_tbl[unit_num].alt = unit_data.alt
units_tbl[unit_num].alt_type = unit_data.alt_type
units_tbl[unit_num].speed = unit_data.speed
units_tbl[unit_num].livery_id = unit_data.livery_id
if unit_data.point then --ME currently does not work like this, but it might one day
units_tbl[unit_num].point = unit_data.point
else
units_tbl[unit_num].point = {}
units_tbl[unit_num].point.x = unit_data.x
units_tbl[unit_num].point.y = unit_data.y
end
units_tbl[unit_num].x = unit_data.x
units_tbl[unit_num].y = unit_data.y
units_tbl[unit_num].callsign = unit_data.callsign
units_tbl[unit_num].onboard_num = unit_data.onboard_num
units_tbl[unit_num].hardpoint_racks = unit_data.hardpoint_racks
units_tbl[unit_num].psi = unit_data.psi
if helipadId then
units_tbl[unit_num].helipadId = mist.utils.deepCopy(helipadId)
end
if airdromeId then
units_tbl[unit_num].airdromeId = mist.utils.deepCopy(airdromeId)
end
units_tbl[unit_num].groupName = groupName
units_tbl[unit_num].groupId = group_data.groupId
units_tbl[unit_num].linkUnit = unit_data.linkUnit
if unit_data.AddPropAircraft then
units_tbl[unit_num].AddPropAircraft = unit_data.AddPropAircraft
end
if category == 'static' then
units_tbl[unit_num].categoryStatic = unit_data.category
units_tbl[unit_num].shape_name = unit_data.shape_name
if group_data.linkOffset then
if group_data.route and group_data.route.points and group_data.route.points[1] and group_data.route.points[1].linkUnit then
units_tbl[unit_num].linkUnit = group_data.route.points[1].linkUnit
end
units_tbl[unit_num].offset = unit_data.offsets
end
if unit_data.mass then
units_tbl[unit_num].mass = unit_data.mass
end
if unit_data.canCargo then
units_tbl[unit_num].canCargo = unit_data.canCargo
end
if unit_data.category == "Heliports" then
if not abRef.units[unit_data.unitId] then
abRef.units[unit_data.unitId] = {name = unit_data.name}
end
end
end
end --for unit_num, unit_data in pairs(group_data.units) do
end --if group_data and group_data.units then
end --for group_num, group_data in pairs(obj_cat_data.group) do
end --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then
end --if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then
end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do
end --if type(cntry_data) == 'table' then
end --for cntry_id, cntry_data in pairs(coa_data.country) do
end --if coa_data.country then --there is a country table
end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then
end --for coa_name, coa_data in pairs(mission.coalition) do
mist.DBs.unitsByName = {}
mist.DBs.unitsById = {}
mist.DBs.unitsByCat = {}
mist.DBs.unitsByCat.helicopter = {} -- adding default categories
mist.DBs.unitsByCat.plane = {}
mist.DBs.unitsByCat.ship = {}
mist.DBs.unitsByCat.static = {}
mist.DBs.unitsByCat.vehicle = {}
mist.DBs.unitsByNum = {}
mist.DBs.groupsByName = {}
mist.DBs.groupsById = {}
mist.DBs.humansByName = {}
mist.DBs.humansById = {}
mist.DBs.dynGroupsAdded = {} -- will be filled by mist.dbUpdate from dynamically spawned groups
mist.DBs.activeHumans = {}
mist.DBs.aliveUnits = {} -- will be filled in by the "updateAliveUnits" coroutine in mist.main.
mist.DBs.removedAliveUnits = {} -- will be filled in by the "updateAliveUnits" coroutine in mist.main.
mist.DBs.const = {}
mist.DBs.const.nato = {
a = "alpha",
b = "bravo",
c = "charlie",
d = "delta",
e = "echo",
f = "foxtrot",
g = "golf",
h = "hotel",
i = "india",
j = "juliett",
k = "kilo",
l = "lima",
m = "mike",
n = "november",
o = "oscar",
p = "papa",
q = "quebec",
r = "romeo",
s = "sierra",
t = "tango",
u = "uniform",
v = "victor",
w = "whiskey",
x = "xray",
y = "yankee",
z = "zulu",
}
-- not accessible by SSE, must use static list :-/
mist.DBs.const.callsigns = {
['NATO'] = {
['rules'] = {
['groupLimit'] = 9,
},
['AWACS'] = {
['Overlord'] = 1,
['Magic'] = 2,
['Wizard'] = 3,
['Focus'] = 4,
['Darkstar'] = 5,
},
['TANKER'] = {
['Texaco'] = 1,
['Arco'] = 2,
['Shell'] = 3,
},
['TRANSPORT'] = {
['Heavy'] = 9,
['Trash'] = 10,
['Cargo'] = 11,
['Ascot'] = 12,
['JTAC'] = {
['Axeman'] = 1,
['Darknight'] = 2,
['Warrior'] = 3,
['Pointer'] = 4,
['Eyeball'] = 5,
['Moonbeam'] = 6,
['Whiplash'] = 7,
['Finger'] = 8,
['Pinpoint'] = 9,
['Ferret'] = 10,
['Shaba'] = 11,
['Playboy'] = 12,
['Hammer'] = 13,
['Jaguar'] = 14,
['Deathstar'] = 15,
['Anvil'] = 16,
['Firefly'] = 17,
['Mantis'] = 18,
['Badger'] = 19,
},
['aircraft'] = {
['Enfield'] = 1,
['Springfield'] = 2,
['Uzi'] = 3,
['Colt'] = 4,
['Dodge'] = 5,
['Ford'] = 6,
['Chevy'] = 7,
['Pontiac'] = 8,
},
['unique'] = {
['A10'] = {
['Hawg'] = 9,
['Boar'] = 10,
['Pig'] = 11,
['Tusk'] = 12,
['rules'] = {
['canUseAircraft'] = true,
['appliesTo'] = {
'A-10C_2',
'A-10C',
'A-10A',
},
},
},
['f16'] = {
Viper = 9,
Venom = 10,
Lobo = 11,
Cowboy = 12,
Python = 13,
Rattler =14,
Panther = 15,
Wolf = 16,
Weasel = 17,
Wild = 18,
Ninja = 19,
Jedi = 20,
rules = {
['canUseAircraft'] = true,
['appliesTo'] = {
'F-16C_50',
'F-16C bl.52d',
'F-16C bl.50',
'F-16A MLU',
'F-16A',
},
},
},
['f18'] = {
['Hornet'] = 9,
['Squid'] = 10,
['Ragin'] = 11,
['Roman'] = 12,
Sting = 13,
Jury =14,
Jokey = 15,
Ram = 16,
Hawk = 17,
Devil = 18,
Check = 19,
Snake = 20,
['rules'] = {
['canUseAircraft'] = true,
['appliesTo'] = {
"FA-18C_hornet",
'F/A-18C',
},
},
},
['b1'] = {
['Bone'] = 9,
['Dark'] = 10,
['Vader'] = 11,
['rules'] = {
['canUseAircraft'] = true,
['appliesTo'] = {
'B-1B',
},
},
},
['b52'] = {
['Buff'] = 9,
['Dump'] = 10,
['Kenworth'] = 11,
['rules'] = {
['canUseAircraft'] = true,
['appliesTo'] = {
'B-52H',
},
},
},
['f15e'] = {
['Dude'] = 9,
['Thud'] = 10,
['Gunny'] = 11,
['Trek'] = 12,
Sniper = 13,
Sled =14,
Best = 15,
Jazz = 16,
Rage = 17,
Tahoe = 18,
['rules'] = {
['canUseAircraft'] = true,
['appliesTo'] = {
'F-15E',
--'F-15ERAZBAM',
},
},
},
},
},
},
}
mist.DBs.const.shapeNames = {
["Landmine"] = "landmine",
["FARP CP Blindage"] = "kp_ug",
["Subsidiary structure C"] = "saray-c",
["Barracks 2"] = "kazarma2",
["Small house 2C"] = "dom2c",
["Military staff"] = "aviashtab",
["Tech hangar A"] = "ceh_ang_a",
["Oil derrick"] = "neftevyshka",
["Tech combine"] = "kombinat",
["Garage B"] = "garage_b",
["Airshow_Crowd"] = "Crowd1",
["Hangar A"] = "angar_a",
["Repair workshop"] = "tech",
["Subsidiary structure D"] = "saray-d",
["FARP Ammo Dump Coating"] = "SetkaKP",
["Small house 1C area"] = "dom2c-all",
["Tank 2"] = "airbase_tbilisi_tank_01",
["Boiler-house A"] = "kotelnaya_a",
["Workshop A"] = "tec_a",
["Small werehouse 1"] = "s1",
["Garage small B"] = "garagh-small-b",
["Small werehouse 4"] = "s4",
["Shop"] = "magazin",
["Subsidiary structure B"] = "saray-b",
["FARP Fuel Depot"] = "GSM Rus",
["Coach cargo"] = "wagon-gruz",
["Electric power box"] = "tr_budka",
["Tank 3"] = "airbase_tbilisi_tank_02",
["Red_Flag"] = "H-flag_R",
["Container red 3"] = "konteiner_red3",
["Garage A"] = "garage_a",
["Hangar B"] = "angar_b",
["Black_Tyre"] = "H-tyre_B",
["Cafe"] = "stolovaya",
["Restaurant 1"] = "restoran1",
["Subsidiary structure A"] = "saray-a",
["Container white"] = "konteiner_white",
["Warehouse"] = "sklad",
["Tank"] = "bak",
["Railway crossing B"] = "pereezd_small",
["Subsidiary structure F"] = "saray-f",
["Farm A"] = "ferma_a",
["Small werehouse 3"] = "s3",
["Water tower A"] = "wodokachka_a",
["Railway station"] = "r_vok_sd",
["Coach a tank blue"] = "wagon-cisterna_blue",
["Supermarket A"] = "uniwersam_a",
["Coach a platform"] = "wagon-platforma",
["Garage small A"] = "garagh-small-a",
["TV tower"] = "tele_bash",
["Comms tower M"] = "tele_bash_m",
["Small house 1A"] = "domik1a",
["Farm B"] = "ferma_b",
["GeneratorF"] = "GeneratorF",
["Cargo1"] = "ab-212_cargo",
["Container red 2"] = "konteiner_red2",
["Subsidiary structure E"] = "saray-e",
["Coach a passenger"] = "wagon-pass",
["Black_Tyre_WF"] = "H-tyre_B_WF",
["Electric locomotive"] = "elektrowoz",
["Shelter"] = "ukrytie",
["Coach a tank yellow"] = "wagon-cisterna_yellow",
["Railway crossing A"] = "pereezd_big",
[".Ammunition depot"] = "SkladC",
["Small werehouse 2"] = "s2",
["Windsock"] = "H-Windsock_RW",
["Shelter B"] = "ukrytie_b",
["Fuel tank"] = "toplivo-bak",
["Locomotive"] = "teplowoz",
[".Command Center"] = "ComCenter",
["Pump station"] = "nasos",
["Black_Tyre_RF"] = "H-tyre_B_RF",
["Coach cargo open"] = "wagon-gruz-otkr",
["Subsidiary structure 3"] = "hozdomik3",
["FARP Tent"] = "PalatkaB",
["White_Tyre"] = "H-tyre_W",
["Subsidiary structure G"] = "saray-g",
["Container red 1"] = "konteiner_red1",
["Small house 1B area"] = "domik1b-all",
["Subsidiary structure 1"] = "hozdomik1",
["Container brown"] = "konteiner_brown",
["Small house 1B"] = "domik1b",
["Subsidiary structure 2"] = "hozdomik2",
["Chemical tank A"] = "him_bak_a",
["WC"] = "WC",
["Small house 1A area"] = "domik1a-all",
["White_Flag"] = "H-Flag_W",
["Airshow_Cone"] = "Comp_cone",
["Bulk Cargo Ship Ivanov"] = "barge-1",
["Bulk Cargo Ship Yakushev"] = "barge-2",
["Outpost"]="block",
["Road outpost"]="block-onroad",
["Container camo"] = "bw_container_cargo",
["Tech Hangar A"] = "ceh_ang_a",
["Bunker 1"] = "dot",
["Bunker 2"] = "dot2",
["Tanker Elnya 160"] = "elnya",
["F-shape barrier"] = "f_bar_cargo",
["Helipad Single"] = "farp",
["FARP"] = "farps",
["Fueltank"] = "fueltank_cargo",
["Gate"] = "gate",
["Armed house"] = "home1_a",
["FARP Command Post"] = "kp-ug",
["Watch Tower Armed"] = "ohr-vyshka",
["Oiltank"] = "oiltank_cargo",
["Pipes small"] = "pipes_small_cargo",
["Pipes big"] = "pipes_big_cargo",
["Oil platform"] = "plavbaza",
["Tetrapod"] = "tetrapod_cargo",
["Trunks long"] = "trunks_long_cargo",
["Trunks small"] = "trunks_small_cargo",
["Passenger liner"] = "yastrebow",
["Passenger boat"] = "zwezdny",
["Oil rig"] = "oil_platform",
["Gas platform"] = "gas_platform",
["Container 20ft"] = "container_20ft",
["Container 40ft"] = "container_40ft",
["Downed pilot"] = "cadaver",
["Parachute"] = "parash",
["Pilot F15 Parachute"] = "pilot_f15_parachute",
["Pilot standing"] = "pilot_parashut",
}
-- create mist.DBs.oldAliveUnits
-- do
-- local intermediate_alive_units = {} -- between 0 and 0.5 secs old
-- local function make_old_alive_units() -- called every 0.5 secs, makes the old_alive_units DB which is just a copy of alive_units that is 0.5 to 1 sec old
-- if intermediate_alive_units then
-- mist.DBs.oldAliveUnits = mist.utils.deepCopy(intermediate_alive_units)
-- end
-- intermediate_alive_units = mist.utils.deepCopy(mist.DBs.aliveUnits)
-- timer.scheduleFunction(make_old_alive_units, nil, timer.getTime() + 0.5)
-- end
-- make_old_alive_units()
-- end
--Build DBs
--dbLog:echo(abRef)
mist.DBs.spawnsByBase = {}
for coa_name, coa_data in pairs(mist.DBs.units) do
for cntry_name, cntry_data in pairs(coa_data) do
for category_name, category_data in pairs(cntry_data) do
if type(category_data) == 'table' then
for group_ind, group_data in pairs(category_data) do
if type(group_data) == 'table' and group_data.units and type(group_data.units) == 'table' and #group_data.units > 0 then -- OCD paradigm programming
mist.DBs.groupsByName[group_data.groupName] = mist.utils.deepCopy(group_data)
mist.DBs.groupsById[group_data.groupId] = mist.utils.deepCopy(group_data)
for unit_ind, unit_data in pairs(group_data.units) do
local copy = mist.utils.deepCopy(unit_data)
local num = #mist.DBs.unitsByNum + 1
copy.dbNum = num
mist.DBs.unitsByName[unit_data.unitName] = mist.utils.deepCopy(copy)
mist.DBs.unitsById[unit_data.unitId] = mist.utils.deepCopy(copy)
mist.DBs.unitsByCat[unit_data.category] = mist.DBs.unitsByCat[unit_data.category] or {} -- future-proofing against new categories...
table.insert(mist.DBs.unitsByCat[unit_data.category], mist.utils.deepCopy(copy))
--dbLog:info('inserting $1', unit_data.unitName)
table.insert(mist.DBs.unitsByNum, mist.utils.deepCopy(copy))
if unit_data.skill and (unit_data.skill == "Client" or unit_data.skill == "Player") then
mist.DBs.humansByName[unit_data.unitName] = mist.utils.deepCopy(copy)
mist.DBs.humansById[unit_data.unitId] = mist.utils.deepCopy(copy)
--if Unit.getByName(unit_data.unitName) then
-- mist.DBs.activeHumans[unit_data.unitName] = mist.utils.deepCopy(unit_data)
-- mist.DBs.activeHumans[unit_data.unitName].playerName = Unit.getByName(unit_data.unitName):getPlayerName()
--end
end
if unit_data.airdromeId then
--log:echo(unit_data.airdromeId)
--log:echo(abRef.airbase[unit_data.airdromeId])
if not mist.DBs.spawnsByBase[abRef.airbase[unit_data.airdromeId].name] then
mist.DBs.spawnsByBase[abRef.airbase[unit_data.airdromeId].name] = {}
end
table.insert(mist.DBs.spawnsByBase[abRef.airbase[unit_data.airdromeId].name], unit_data.unitName)
end
if unit_data.helipadId and abRef.units[unit_data.helipadId] and abRef.units[unit_data.helipadId].name then
if not mist.DBs.spawnsByBase[abRef.units[unit_data.helipadId].name] then
mist.DBs.spawnsByBase[abRef.units[unit_data.helipadId].name] = {}
end
table.insert(mist.DBs.spawnsByBase[abRef.units[unit_data.helipadId].name], unit_data.unitName)
end
end
end
end
end
end
end
end
mist.DBs.zonesByName = {}
mist.DBs.zonesByNum = {}
if env.mission.triggers and env.mission.triggers.zones then
for zone_ind, zone_data in pairs(env.mission.triggers.zones) do
if type(zone_data) == 'table' then
local zone = mist.utils.deepCopy(zone_data)
--log:warn(zone)
zone.point = {} -- point is used by SSE
zone.point.x = zone_data.x
zone.point.y = land.getHeight({x = zone_data.x, y = zone_data.y})
zone.point.z = zone_data.y
zone.properties = {}
if zone_data.properties then
for propInd, prop in pairs(zone_data.properties) do
if prop.value and tostring(prop.value) ~= "" then
zone.properties[prop.key] = prop.value
end
end
end
if zone.verticies then -- trust but verify
local r = 0
for i = 1, #zone.verticies do
local dist = mist.utils.get2DDist(zone.point, zone.verticies[i])
if dist > r then
r = mist.utils.deepCopy(dist)
end
end
zone.radius = r
end
if zone.linkUnit then
local uRef = mist.DBs.unitsByName[zone.linkUnit]
if uRef then
if zone.verticies then
local offset = {}
for i = 1, #zone.verticies do
table.insert(offset, {dist = mist.utils.get2DDist(uRef.point, zone.verticies[i]), heading = mist.getHeadingPoints(uRef.point, zone.verticies[i]) + uRef.heading})
end
zone.offset = offset
else
zone.offset = {dist = mist.utils.get2DDist(uRef.point, zone.point), heading = mist.getHeadingPoints(uRef.point, zone.point) + uRef.heading}
end
end
end
mist.DBs.zonesByName[zone_data.name] = zone
mist.DBs.zonesByNum[#mist.DBs.zonesByNum + 1] = mist.utils.deepCopy(zone) --[[deepcopy so that the zone in zones_by_name and the zone in
zones_by_num se are different objects.. don't want them linked.]]
end
end
end
--DynDBs
mist.DBs.MEunits = mist.utils.deepCopy(mist.DBs.units)
mist.DBs.MEunitsByName = mist.utils.deepCopy(mist.DBs.unitsByName)
mist.DBs.MEunitsById = mist.utils.deepCopy(mist.DBs.unitsById)
mist.DBs.MEunitsByCat = mist.utils.deepCopy(mist.DBs.unitsByCat)
mist.DBs.MEunitsByNum = mist.utils.deepCopy(mist.DBs.unitsByNum)
mist.DBs.MEgroupsByName = mist.utils.deepCopy(mist.DBs.groupsByName)
mist.DBs.MEgroupsById = mist.utils.deepCopy(mist.DBs.groupsById)
mist.DBs.deadObjects = {}
do
local mt = {}
function mt.__newindex(t, key, val)
local original_key = key --only for duplicate runtime IDs.
local key_ind = 1
while mist.DBs.deadObjects[key] do
--dbLog:warn('duplicate runtime id of previously dead object key: $1', key)
key = tostring(original_key) .. ' #' .. tostring(key_ind)
key_ind = key_ind + 1
end
if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then
----dbLog:info('object found in alive_units')
val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_])
local pos = Object.getPosition(val.object)
if pos then
val.objectPos = pos.p
end
val.objectType = mist.DBs.aliveUnits[val.object.id_].category
elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then -- it didn't exist in alive_units, check old_alive_units
----dbLog:info('object found in old_alive_units')
val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])
local pos = Object.getPosition(val.object)
if pos then
val.objectPos = pos.p
end
val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category
else --attempt to determine if static object...
----dbLog:info('object not found in alive units or old alive units')
local pos = Object.getPosition(val.object)
if pos then
local static_found = false
for ind, static in pairs(mist.DBs.unitsByCat.static) do
if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...
--dbLog:info('correlated dead static object to position')
val.objectData = static
val.objectPos = pos.p
val.objectType = 'static'
static_found = true
break
end
end
if not static_found then
val.objectPos = pos.p
val.objectType = 'building'
val.typeName = Object.getTypeName(val.object)
end
else
val.objectType = 'unknown'
end
end
rawset(t, key, val)
end
setmetatable(mist.DBs.deadObjects, mt)
end
do -- mist unitID funcs
for id, idData in pairs(mist.DBs.unitsById) do
if idData.unitId > mist.nextUnitId then
mist.nextUnitId = mist.utils.deepCopy(idData.unitId)
end
if idData.groupId > mist.nextGroupId then
mist.nextGroupId = mist.utils.deepCopy(idData.groupId)
end
end
end
end
local function updateAliveUnits() -- coroutine function
--log:warn("updateALiveUnits")
local lalive_units = mist.DBs.aliveUnits -- local references for faster execution
local lunits = mist.DBs.unitsByNum
local ldeepcopy = mist.utils.deepCopy
local lUnit = Unit
local lremovedAliveUnits = mist.DBs.removedAliveUnits
local updatedUnits = {}
if #lunits > 0 then
local units_per_run = math.ceil(#lunits/20)
if units_per_run < 5 then
units_per_run = 5
end
for i = 1, #lunits do
if lunits[i].category ~= 'static' then -- can't get statics with Unit.getByName :(
local unit = lUnit.getByName(lunits[i].unitName)
if unit and unit:isExist() == true then
----dbLog:info("unit named $1 alive!", lunits[i].unitName) -- spammy
local pos = unit:getPosition()
local newtbl = ldeepcopy(lunits[i])
if pos then
newtbl.pos = pos.p
end
newtbl.unit = unit
--newtbl.rt_id = unit.id_
lalive_units[unit.id_] = newtbl
updatedUnits[unit.id_] = true
end
end
if i%units_per_run == 0 then
--log:warn("yield: $1", i)
coroutine.yield()
end
end
-- All units updated, remove any "alive" units that were not updated- they are dead!
for unit_id, unit in pairs(lalive_units) do
if not updatedUnits[unit_id] then
lremovedAliveUnits[unit_id] = unit
lalive_units[unit_id] = nil
end
end
end
end
local function dbUpdate(event, oType, origGroupName)
--dbLog:info('dbUpdate: $1', event)
local newTable = {}
local objType = oType
newTable.startTime = 0
if type(event) == 'string' then -- if name of an object.
local newObject
if Group.getByName(event) then
newObject = Group.getByName(event)
elseif StaticObject.getByName(event) then
newObject = StaticObject.getByName(event)
objType = "static"
-- log:info('its static')
else
log:warn('$1 is not a Group or Static Object. This should not be possible. Sent category is: $2', event, objType)
return false
end
local objName = newObject:getName()
newTable.name = origGroupName or objName
newTable.groupId = tonumber(newObject:getID())
newTable.groupName = origGroupName or objName
local unitOneRef
if objType == 'static' then
unitOneRef = newObject
newTable.countryId = tonumber(newObject:getCountry())
newTable.coalitionId = tonumber(newObject:getCoalition())
newTable.category = 'static'
else
unitOneRef = newObject:getUnits()
if #unitOneRef > 0 and unitOneRef[1] and type(unitOneRef[1]) == 'table' then
newTable.countryId = tonumber(unitOneRef[1]:getCountry())
newTable.coalitionId = tonumber(unitOneRef[1]:getCoalition())
newTable.category = tonumber(Object.getCategory(newObject))
else
log:warn('getUnits failed to return on $1 ; Built Data: $2.', event, newTable)
return false
end
end
for countryData, countryId in pairs(country.id) do
if newTable.country and string.upper(countryData) == string.upper(newTable.country) or countryId == newTable.countryId then
newTable.countryId = countryId
newTable.country = string.lower(countryData)
for coaData, coaId in pairs(coalition.side) do
if coaId == coalition.getCountryCoalition(countryId) then
newTable.coalition = string.lower(coaData)
end
end
end
end
for catData, catId in pairs(Unit.Category) do
if objType == 'group' and Group.getByName(newTable.groupName):isExist() then
if catId == Group.getByName(newTable.groupName):getCategory() then
newTable.category = string.lower(catData)
end
elseif objType == 'static' and StaticObject.getByName(newTable.groupName):isExist() then
if catId == StaticObject.getByName(newTable.groupName):getCategory() then
newTable.category = string.lower(catData)
end
end
end
local gfound = false
for index, data in pairs(mistAddedGroups) do
if mist.stringMatch(data.name, newTable.groupName) == true then
gfound = true
newTable.task = data.task
newTable.modulation = data.modulation
newTable.uncontrolled = data.uncontrolled
newTable.radioSet = data.radioSet
newTable.hidden = data.hidden
newTable.startTime = data.start_time
mistAddedGroups[index] = nil
end
end
if gfound == false then
newTable.uncontrolled = false
newTable.hidden = false
end
newTable.units = {}
if objType == 'group' then
for unitId, unitData in pairs(unitOneRef) do
local point = unitData:getPoint()
newTable.units[unitId] = {}
newTable.units[unitId].unitName = unitData:getName()
newTable.units[unitId].x = mist.utils.round(point.x)
newTable.units[unitId].y = mist.utils.round(point.z)
newTable.units[unitId].point = {}
newTable.units[unitId].point.x = newTable.units[unitId].x
newTable.units[unitId].point.y = newTable.units[unitId].y
newTable.units[unitId].alt = mist.utils.round(point.y)
newTable.units[unitId].speed = mist.vec.mag(unitData:getVelocity())
newTable.units[unitId].heading = mist.getHeading(unitData, true)
newTable.units[unitId].type = unitData:getTypeName()
newTable.units[unitId].unitId = tonumber(unitData:getID())
newTable.units[unitId].groupName = newTable.groupName
newTable.units[unitId].groupId = newTable.groupId
newTable.units[unitId].countryId = newTable.countryId
newTable.units[unitId].coalitionId = newTable.coalitionId
newTable.units[unitId].coalition = newTable.coalition
newTable.units[unitId].country = newTable.country
local found = false
for index, data in pairs(mistAddedObjects) do
if mist.stringMatch(data.name, newTable.units[unitId].unitName) == true then
found = true
newTable.units[unitId].livery_id = data.livery_id
newTable.units[unitId].skill = data.skill
newTable.units[unitId].alt_type = data.alt_type
newTable.units[unitId].callsign = data.callsign
newTable.units[unitId].psi = data.psi
mistAddedObjects[index] = nil
end
if found == false then
newTable.units[unitId].skill = "High"
newTable.units[unitId].alt_type = "BARO"
end
if newTable.units[unitId].alt_type == "RADIO" then -- raw postition MSL was grabbed for group, but spawn is AGL, so re-offset it
newTable.units[unitId].alt = (newTable.units[unitId].alt - land.getHeight({x = newTable.units[unitId].x, y = newTable.units[unitId].y}))
end
end
end
else -- its a static
newTable.category = 'static'
local point = newObject:getPoint()
newTable.units[1] = {}
newTable.units[1].unitName = newObject:getName()
newTable.units[1].category = 'static'
newTable.units[1].x = mist.utils.round(point.x)
newTable.units[1].y = mist.utils.round(point.z)
newTable.units[1].point = {}
newTable.units[1].point.x = newTable.units[1].x
newTable.units[1].point.y = newTable.units[1].y
newTable.units[1].alt = mist.utils.round(point.y)
newTable.units[1].heading = mist.getHeading(newObject, true)
newTable.units[1].type = newObject:getTypeName()
newTable.units[1].unitId = tonumber(newObject:getID())
newTable.units[1].groupName = newTable.name
newTable.units[1].groupId = newTable.groupId
newTable.units[1].countryId = newTable.countryId
newTable.units[1].country = newTable.country
newTable.units[1].coalitionId = newTable.coalitionId
newTable.units[1].coalition = newTable.coalition
if Object.getCategory(newObject) == 6 and newObject:getCargoDisplayName() then
local mass = newObject:getCargoDisplayName()
mass = string.gsub(mass, ' ', '')
mass = string.gsub(mass, 'kg', '')
newTable.units[1].mass = tonumber(mass)
newTable.units[1].categoryStatic = 'Cargos'
newTable.units[1].canCargo = true
newTable.units[1].shape_name = 'ab-212_cargo'
end
----- search mist added objects for extra data if applicable
for index, data in pairs(mistAddedObjects) do
if mist.stringMatch(data.name, newTable.units[1].unitName) == true then
newTable.units[1].shape_name = data.shape_name -- for statics
newTable.units[1].livery_id = data.livery_id
newTable.units[1].airdromeId = data.airdromeId
newTable.units[1].mass = data.mass
newTable.units[1].canCargo = data.canCargo
newTable.units[1].categoryStatic = data.categoryStatic
newTable.units[1].type = data.type
newTable.units[1].linkUnit = data.linkUnit
mistAddedObjects[index] = nil
break
end
end
end
end
--dbLog:warn(newTable)
--mist.debug.writeData(mist.utils.serialize,{'msg', newTable}, timer.getAbsTime() ..'Group.lua')
newTable.timeAdded = timer.getAbsTime() -- only on the dynGroupsAdded table. For other reference, see start time
--mist.debug.dumpDBs()
--end
--dbLog:info('endDbUpdate')
return newTable
end
--[[DB update code... FRACK. I need to refactor some of it.
The problem is that the DBs need to account better for shared object names. Needs to write over some data and outright remove other.
If groupName is used then entire group needs to be rewritten
what to do with old groups units DB entries?. Names cant be assumed to be the same.
-- new spawn event check.
-- event handler filters everything into groups: tempSpawnedGroups
-- this function then checks DBs to see if data has changed
]]
local function checkSpawnedEventsNew()
if tempSpawnGroupsCounter > 0 then
--[[local updatesPerRun = math.ceil(#tempSpawnedGroupsCounter/20)
if updatesPerRun < 5 then
updatesPerRun = 5
end]]
--dbLog:info('iterate')
for name, gData in pairs(tempSpawnedGroups) do
--env.info(name)
--dbLog:warn(gData)
local updated = false
local stillExists = false
local staticGroupName
if not gData.checked then
tempSpawnedGroups[name].checked = true -- so if there was an error it will get cleared.
local _g = gData.gp or Group.getByName(name)
if mist.DBs.groupsByName[name] then
-- first check group level properties, groupId, countryId, coalition
--dbLog:info('Found in DBs, check if updated')
local dbTable = mist.DBs.groupsByName[name]
--dbLog:info(dbTable)
if gData.type ~= 'static' then
--dbLog:info('Not static')
if _g and _g:isExist() == true then
stillExists = true
local _u = _g:getUnit(1)
if _u and (dbTable.groupId ~= tonumber(_g:getID()) or _u:getCountry() ~= dbTable.countryId or _u:getCoalition() ~= dbTable.coaltionId) then
--dbLog:info('Group Data mismatch')
updated = true
else
-- dbLog:info('No Mismatch')
end
else
dbLog:warn('$1 : Group was not accessible', name)
end
end
end
--dbLog:info('Updated: $1', updated)
if updated == false then
if gData.type ~= 'static' then -- time to check units
-- dbLog:info('No Group Mismatch, Check Units')
if _g and _g:isExist() == true then
stillExists = true
for index, uObject in pairs(_g:getUnits()) do
-- dbLog:info(index)
if mist.DBs.unitsByName[uObject:getName()] then
--dbLog:info('UnitByName table exists')
local uTable = mist.DBs.unitsByName[uObject:getName()]
if tonumber(uObject:getID()) ~= uTable.unitId or uObject:getTypeName() ~= uTable.type then
--dbLog:info('Unit Data mismatch')
updated = true
break
end
end
end
end
else -- it is a static object
local ref = mist.DBs.unitsByName[name]
if ref then
staticGroupName = ref.groupName
else
stillExists = true
end
end
else
stillExists = true
end
if stillExists == true and (updated == true or not mist.DBs.groupsByName[name]) then
--dbLog:info('Get Table')
local dbData = dbUpdate(name, gData.type, staticGroupName)
if dbData and type(dbData) == 'table' then
writeGroups[#writeGroups+1] = {data = dbData, isUpdated = updated}
end
end
-- Work done, so remove
end
tempSpawnedGroups[name] = nil
tempSpawnGroupsCounter = tempSpawnGroupsCounter - 1
end
end
end
local updateChecker = {}
local function writeDBTables(newEntry)
local ldeepCopy = mist.utils.deepCopy
local newTable = newEntry.data
--dbLog:info(newTable)
local state = 0
if updateChecker[newTable.name] then
dbLog:warn("Failed to add to database: $1. Stopped at state: $2", newTable.name, updateChecker[newTable.name])
return false
else
--dbLog:info('define default state')
updateChecker[newTable.name] = 0
--dbLog:info('define default state1')
state = updateChecker[newTable.name]
--dbLog:info('define default state2')
end
local updated = newEntry.isUpdated
local mistCategory
--dbLog:info('define categoryy')
if type(newTable.category) == 'string' then
mistCategory = string.lower(newTable.category)
end
if string.upper(newTable.category) == 'GROUND_UNIT' then
mistCategory = 'vehicle'
newTable.category = mistCategory
elseif string.upper(newTable.category) == 'AIRPLANE' then
mistCategory = 'plane'
newTable.category = mistCategory
elseif string.upper(newTable.category) == 'HELICOPTER' then
mistCategory = 'helicopter'
newTable.category = mistCategory
elseif string.upper(newTable.category) == 'SHIP' then
mistCategory = 'ship'
newTable.category = mistCategory
end
--dbLog:info('Update unitsBy')
state = 1
for newId, newUnitData in pairs(newTable.units) do
--dbLog:info(newId)
newUnitData.category = mistCategory
--dbLog:info(updated)
if mist.DBs.unitsByName[newUnitData.unitName] and updated == true then --if unit existed before and something was updated, write over the entry for a given unit name just in case.
state = 1.1
--dbLog:info('Updating Unit Tables')
local refNum = mist.DBs.unitsByName[newUnitData.unitName].dbNum
for i = 1, #mist.DBs.unitsByCat[mistCategory] do
if mist.DBs.unitsByCat[mistCategory][i].unitName == newUnitData.unitName then
--dbLog:info('Entry Found, Rewriting for unitsByCat')
mist.DBs.unitsByCat[mistCategory][i] = ldeepCopy(newUnitData)
break
end
end
state = 1.2
--dbLog:info('updateByNum')
if refNum then -- easy way
--dbLog:info('refNum exists, Rewriting for unitsByCat')
mist.DBs.unitsByNum[refNum] = ldeepCopy(newUnitData)
else --- the hard way
--dbLog:info('iterate unitsByNum')
for i = 1, #mist.DBs.unitsByNum do
if mist.DBs.unitsByNum[i].unitName == newUnitData.unitName then
--dbLog:info('Entry Found, Rewriting for unitsByNum')
mist.DBs.unitsByNum[i] = ldeepCopy(newUnitData)
break
end
end
end
else
state = 1.3
--dbLog:info('Unitname not in use, add as normal')
newUnitData.dbNum = #mist.DBs.unitsByNum + 1
mist.DBs.unitsByCat[mistCategory][#mist.DBs.unitsByCat[mistCategory] + 1] = ldeepCopy(newUnitData)
mist.DBs.unitsByNum[#mist.DBs.unitsByNum + 1] = ldeepCopy(newUnitData)
end
if newUnitData.unitId then
--dbLog:info('byId')
mist.DBs.unitsById[tonumber(newUnitData.unitId)] = ldeepCopy(newUnitData)
end
mist.DBs.unitsByName[newUnitData.unitName] = ldeepCopy(newUnitData)
end
-- this is a really annoying DB to populate. Gotta create new tables in case its missing
--dbLog:info('write mist.DBs.units')
state = 2
if not mist.DBs.units[newTable.coalition] then
mist.DBs.units[newTable.coalition] = {}
end
state = 3
if not mist.DBs.units[newTable.coalition][newTable.country] then
mist.DBs.units[newTable.coalition][(newTable.country)] = {}
mist.DBs.units[newTable.coalition][(newTable.country)].countryId = newTable.countryId
end
state = 4
if not mist.DBs.units[newTable.coalition][newTable.country][mistCategory] then
mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] = {}
end
state = 5
if updated == true then
--dbLog:info('Updating DBsUnits')
for i = 1, #mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] do
if mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i].groupName == newTable.groupName then
--dbLog:info('Entry Found, Rewriting')
mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i] = ldeepCopy(newTable)
break
end
end
else
--dbLog:info('adding to DBs Units')
mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][#mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] + 1] = ldeepCopy(newTable)
end
state = 6
if newTable.groupId then
--dbLog:info('Make groupsById')
mist.DBs.groupsById[newTable.groupId] = ldeepCopy(newTable)
end
--dbLog:info('make groupsByName')
mist.DBs.groupsByName[newTable.name] = ldeepCopy(newTable)
--dbLog:info('add to dynGroups')
mist.DBs.dynGroupsAdded[#mist.DBs.dynGroupsAdded + 1] = ldeepCopy(newTable)
--dbLog:info('clear entry')
updateChecker[newTable.name] = nil
--dbLog:info('return')
return true
end
function mist.forceAddToDB(object)
-- object is static object or group.
-- call dbUpdate to get the table
local tbl = dbUpdate(object)
if tbl then
local res = writeDBTables(tbl)
if not res then
log:warn("Failed to force add to DBs: $1", object)
end
end
-- call writeDBTables with that table.
end
local function updateDBTables()
local i = #writeGroups
local savesPerRun = math.ceil(i/10)
if savesPerRun < 5 then
savesPerRun = 5
end
if i > 0 then
--dbLog:info('updateDBTables: $1', #writeGroups)
for x = 1, i do
local res = writeDBTables(writeGroups[x])
if res and res == true then
--dbLog:info('result: complete')
writeGroups[x] = nil
else
writeGroups[x] = nil
end
end
if x%savesPerRun == 0 then
coroutine.yield()
end
if timer.getTime() > lastUpdateTime then
lastUpdateTime = timer.getTime()
end
--dbLog:info('endUpdateTables')
end
end
local function groupSpawned(event)
-- dont need to add units spawned in at the start of the mission if mist is loaded in init line
if event.id == world.event.S_EVENT_BIRTH and timer.getTime0() < timer.getAbsTime() then
if Object.getCategory(event.initiator) == 1 and not Unit.getPlayerName(event.initiator) then -- simple player check, will need to later check to see if unit was spawned with a player in a flight
--log:info('Object is a Unit')
if Unit.getGroup(event.initiator) then
-- log:info(Unit.getGroup(event.initiator):getName())
local g = Unit.getGroup(event.initiator)
if not tempSpawnedGroups[g:getName()] then
--log:info('added')
tempSpawnedGroups[g:getName()] = {type = 'group', gp = g}
tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1
end
else
log:error('Group not accessible by unit in event handler. This is a DCS bug')
end
elseif Object.getCategory(event.initiator) == 3 or Object.getCategory(event.initiator) == 6 then
--log:info('staticSpawnEvent')
--log:info(event)
--log:info(event.initiator:getTypeName())
--table.insert(tempSpawnedUnits,(event.initiator))
-------
-- New functionality below.
-------
--log:info(event.initiator:getName())
--log:info('Object is Static')
tempSpawnedGroups[StaticObject.getName(event.initiator)] = {type = 'static'}
tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1
end
end
end
local function doScheduledFunctions()
local i = 1
while i <= #scheduledTasks do
local refTime = timer.getTime()
if not scheduledTasks[i].rep then -- not a repeated process
if scheduledTasks[i].t <= refTime then
local task = scheduledTasks[i] -- local reference
table.remove(scheduledTasks, i)
local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))
if not err then
log:error('Error in scheduled function: $1', errmsg)
end
--task.f(unpack(task.vars, 1, table.maxn(task.vars))) -- do the task, do not increment i
else
i = i + 1
end
else
if scheduledTasks[i].st and scheduledTasks[i].st <= refTime then --if a stoptime was specified, and the stop time exceeded
table.remove(scheduledTasks, i) -- stop time exceeded, do not execute, do not increment i
elseif scheduledTasks[i].t <= refTime then
local task = scheduledTasks[i] -- local reference
task.t = timer.getTime() + task.rep --schedule next run
local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))
if not err then
log:error('Error in scheduled function: $1', errmsg)
end
--scheduledTasks[i].f(unpack(scheduledTasks[i].vars, 1, table.maxn(scheduledTasks[i].vars))) -- do the task
i = i + 1
else
i = i + 1
end
end
end
end
-- Event handler to start creating the dead_objects table
local function addDeadObject(event)
if event.id == world.event.S_EVENT_DEAD or event.id == world.event.S_EVENT_CRASH then
if event.initiator and event.initiator.id_ and event.initiator.id_ > 0 then
local id = event.initiator.id_ -- initial ID, could change if there is a duplicate id_ already dead.
local val = {object = event.initiator} -- the new entry in mist.DBs.deadObjects.
local original_id = id --only for duplicate runtime IDs.
local id_ind = 1
while mist.DBs.deadObjects[id] do
--log:info('duplicate runtime id of previously dead object id: $1', id)
id = tostring(original_id) .. ' #' .. tostring(id_ind)
id_ind = id_ind + 1
end
local valid
if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then
--log:info('object found in alive_units')
val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_])
local pos = Object.getPosition(val.object)
if pos then
val.objectPos = pos.p
end
val.objectType = mist.DBs.aliveUnits[val.object.id_].category
--[[if mist.DBs.activeHumans[Unit.getName(val.object)] then
--trigger.action.outText('remove via death: ' .. Unit.getName(val.object),20)
mist.DBs.activeHumans[Unit.getName(val.object)] = nil
end]]
valid = true
elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then -- it didn't exist in alive_units, check old_alive_units
--log:info('object found in old_alive_units')
val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])
local pos = Object.getPosition(val.object)
if pos then
val.objectPos = pos.p
end
val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category
valid = true
else --attempt to determine if static object...
--log:info('object not found in alive units or old alive units')
if Object.isExist(val.object) then
local pos = Object.getPosition(val.object)
if pos then
local static_found = false
for ind, static in pairs(mist.DBs.unitsByCat.static) do
if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...
--log:info('correlated dead static object to position')
val.objectData = static
val.objectPos = pos.p
val.objectType = 'static'
static_found = true
break
end
end
if not static_found then
val.objectPos = pos.p
val.objectType = 'building'
val.typeName = Object.getTypeName(val.object)
end
else
val.objectType = 'unknown'
end
valid = true
end
end
if valid then
mist.DBs.deadObjects[id] = val
end
end
end
end
--[[
local function addClientsToActive(event)
if event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_BIRTH then
log:info(event)
if Unit.getPlayerName(event.initiator) then
log:info(Unit.getPlayerName(event.initiator))
local newU = mist.utils.deepCopy(mist.DBs.unitsByName[Unit.getName(event.initiator)])
newU.playerName = Unit.getPlayerName(event.initiator)
mist.DBs.activeHumans[Unit.getName(event.initiator)] = newU
--trigger.action.outText('added: ' .. Unit.getName(event.initiator), 20)
end
elseif event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and event.initiator then
if mist.DBs.activeHumans[Unit.getName(event.initiator)] then
mist.DBs.activeHumans[Unit.getName(event.initiator)] = nil
-- trigger.action.outText('removed via control: ' .. Unit.getName(event.initiator), 20)
end
end
end
mist.addEventHandler(addClientsToActive)
]]
local function verifyDB()
--log:warn('verfy Run')
for coaName, coaId in pairs(coalition.side) do
--env.info(coaName)
local gps = coalition.getGroups(coaId)
for i = 1, #gps do
if gps[i] and Group.getSize(gps[i]) > 0 then
local gName = Group.getName(gps[i])
if not mist.DBs.groupsByName[gName] then
--env.info(Unit.getID(gUnits[j]) .. ' Not found in DB yet')
if not tempSpawnedGroups[gName] then
--dbLog:info('added')
tempSpawnedGroups[gName] = {type = 'group', gp = gps[i]}
tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1
end
end
end
end
local st = coalition.getStaticObjects(coaId)
for i = 1, #st do
local s = st[i]
if StaticObject.isExist(s) then
local name = s:getName()
if not mist.DBs.unitsByName[name] then
dbLog:warn('$1 Not found in DB yet. ID: $2', name, StaticObject.getID(s))
if string.len(name) > 0 then -- because in this mission someone sent the name was returning as an empty string. Gotta be careful.
tempSpawnedGroups[s:getName()] = {type = 'static'}
tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1
end
end
end
end
end
end
--- init function.
-- creates logger, adds default event handler
-- and calls main the first time.
-- @function mist.init
function mist.init()
-- create logger
mist.log = mist.Logger:new("MIST", mistSettings.logLevel)
dbLog = mist.Logger:new('MISTDB', mistSettings.dbLog)
log = mist.log -- log shorthand
-- set warning log level, showing only
-- warnings and errors
--log:setLevel("warning")
log:info("initializing databases")
initDBs()
-- add event handler for group spawns
mist.addEventHandler(groupSpawned)
mist.addEventHandler(addDeadObject)
log:warn('Init time: $1', timer.getTime())
-- call main the first time therafter it reschedules itself.
mist.main()
--log:msg('MIST version $1.$2.$3 loaded', mist.majorVersion, mist.minorVersion, mist.build)
mist.scheduleFunction(verifyDB, {}, timer.getTime() + 1)
return
end
--- The main function.
-- Run 100 times per second.
-- You shouldn't call this function.
function mist.main()
timer.scheduleFunction(mist.main, {}, timer.getTime() + 0.01) --reschedule first in case of Lua error
updateTenthSecond = updateTenthSecond + 1
if updateTenthSecond == 20 then
updateTenthSecond = 0
checkSpawnedEventsNew()
if not coroutines.updateDBTables then
coroutines.updateDBTables = coroutine.create(updateDBTables)
end
coroutine.resume(coroutines.updateDBTables)
if coroutine.status(coroutines.updateDBTables) == 'dead' then
coroutines.updateDBTables = nil
end
end
--updating alive units
updateAliveUnitsCounter = updateAliveUnitsCounter + 1
if updateAliveUnitsCounter == 5 then
updateAliveUnitsCounter = 0
if not coroutines.updateAliveUnits then
coroutines.updateAliveUnits = coroutine.create(updateAliveUnits)
end
coroutine.resume(coroutines.updateAliveUnits)
if coroutine.status(coroutines.updateAliveUnits) == 'dead' then
coroutines.updateAliveUnits = nil
end
end
doScheduledFunctions()
end -- end of mist.main
--- Returns next unit id.
-- @treturn number next unit id.
function mist.getNextUnitId()
mist.nextUnitId = mist.nextUnitId + 1
if mist.nextUnitId > 6900 and mist.nextUnitId < 30000 then
mist.nextUnitId = 30000
end
return mist.utils.deepCopy(mist.nextUnitId)
end
--- Returns next group id.
-- @treturn number next group id.
function mist.getNextGroupId()
mist.nextGroupId = mist.nextGroupId + 1
if mist.nextGroupId > 6900 and mist.nextGroupId < 30000 then
mist.nextGroupId = 30000
end
return mist.utils.deepCopy(mist.nextGroupId)
end
--- Returns timestamp of last database update.
-- @treturn timestamp of last database update
function mist.getLastDBUpdateTime()
return lastUpdateTime
end
--- Spawns a static object to the game world.
-- @todo write good docs
-- @tparam table staticObj table containing data needed for the object creation
function mist.dynAddStatic(n)
local newObj = mist.utils.deepCopy(n)
log:warn(newObj)
if newObj.units and newObj.units[1] then -- if its mist format
for entry, val in pairs(newObj.units[1]) do
if newObj[entry] and newObj[entry] ~= val or not newObj[entry] then
newObj[entry] = val
end
end
end
--log:info(newObj)
local cntry = newObj.country
if newObj.countryId then
cntry = newObj.countryId
end
local newCountry = ''
for countryId, countryName in pairs(country.name) do
if type(cntry) == 'string' then
cntry = cntry:gsub("%s+", "_")
if tostring(countryName) == string.upper(cntry) then
newCountry = countryName
end
elseif type(cntry) == 'number' then
if countryId == cntry then
newCountry = countryName
end
end
end
if newCountry == '' then
log:error("Country not found: $1", cntry)
return false
end
if newObj.clone or not newObj.groupId then
mistGpId = mistGpId + 1
newObj.groupId = mistGpId
end
if newObj.clone or not newObj.unitId then
mistUnitId = mistUnitId + 1
newObj.unitId = mistUnitId
end
newObj.name = newObj.name or newObj.unitName
if newObj.clone or not newObj.name then
mistDynAddIndex[' static '] = mistDynAddIndex[' static '] + 1
newObj.name = (newCountry .. ' static ' .. mistDynAddIndex[' static '])
end
if not newObj.dead then
newObj.dead = false
end
if not newObj.heading then
newObj.heading = math.rad(math.random(360))
end
if newObj.categoryStatic then
newObj.category = newObj.categoryStatic
end
if newObj.mass then
newObj.category = 'Cargos'
end
if newObj.shapeName then
newObj.shape_name = newObj.shapeName
end
if not newObj.shape_name then
log:info('shape_name not present')
if mist.DBs.const.shapeNames[newObj.type] then
newObj.shape_name = mist.DBs.const.shapeNames[newObj.type]
end
end
mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newObj)
if newObj.x and newObj.y and newObj.type and type(newObj.x) == 'number' and type(newObj.y) == 'number' and type(newObj.type) == 'string' then
--log:warn(newObj)
coalition.addStaticObject(country.id[newCountry], newObj)
return newObj
end
log:error("Failed to add static object due to missing or incorrect value. X: $1, Y: $2, Type: $3", newObj.x, newObj.y, newObj.type)
return false
end
--- Spawns a dynamic group into the game world.
-- Same as coalition.add function in SSE. checks the passed data to see if its valid.
-- Will generate groupId, groupName, unitId, and unitName if needed
-- @tparam table newGroup table containting values needed for spawning a group.
function mist.dynAdd(ng)
local newGroup = mist.utils.deepCopy(ng)
--log:warn(newGroup)
--mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupOrig.lua')
local cntry = newGroup.country
if newGroup.countryId then
cntry = newGroup.countryId
end
local groupType = newGroup.category
local newCountry = ''
-- validate data
for countryId, countryName in pairs(country.name) do
if type(cntry) == 'string' then
cntry = cntry:gsub("%s+", "_")
if tostring(countryName) == string.upper(cntry) then
newCountry = countryName
end
elseif type(cntry) == 'number' then
if countryId == cntry then
newCountry = countryName
end
end
end
if newCountry == '' then
log:error("Country not found: $1", cntry)
return false
end
local newCat = ''
for catName, catId in pairs(Unit.Category) do
if type(groupType) == 'string' then
if tostring(catName) == string.upper(groupType) then
newCat = catName
end
elseif type(groupType) == 'number' then
if catId == groupType then
newCat = catName
end
end
if catName == 'GROUND_UNIT' and (string.upper(groupType) == 'VEHICLE' or string.upper(groupType) == 'GROUND') then
newCat = 'GROUND_UNIT'
elseif catName == 'AIRPLANE' and string.upper(groupType) == 'PLANE' then
newCat = 'AIRPLANE'
end
end
local typeName
if newCat == 'GROUND_UNIT' then
typeName = ' gnd '
elseif newCat == 'AIRPLANE' then
typeName = ' air '
elseif newCat == 'HELICOPTER' then
typeName = ' hel '
elseif newCat == 'SHIP' then
typeName = ' shp '
elseif newCat == 'BUILDING' then
typeName = ' bld '
end
if newGroup.clone or not newGroup.groupId then
mistDynAddIndex[typeName] = mistDynAddIndex[typeName] + 1
mistGpId = mistGpId + 1
newGroup.groupId = mistGpId
end
if newGroup.groupName or newGroup.name then
if newGroup.groupName then
newGroup.name = newGroup.groupName
elseif newGroup.name then
newGroup.name = newGroup.name
end
end
if newGroup.clone and mist.DBs.groupsByName[newGroup.name] or not newGroup.name then
--if newGroup.baseName then
-- idea of later. So custmozed naming can be created
-- else
newGroup.name = tostring(newCountry .. tostring(typeName) .. mistDynAddIndex[typeName])
--end
end
if not newGroup.hidden then
newGroup.hidden = false
end
if not newGroup.visible then
newGroup.visible = false
end
if (newGroup.start_time and type(newGroup.start_time) ~= 'number') or not newGroup.start_time then
if newGroup.startTime then
newGroup.start_time = mist.utils.round(newGroup.startTime)
else
newGroup.start_time = 0
end
end
for unitIndex, unitData in pairs(newGroup.units) do
local originalName = newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name
if newGroup.clone or not unitData.unitId then
mistUnitId = mistUnitId + 1
newGroup.units[unitIndex].unitId = mistUnitId
end
if newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name then
if newGroup.units[unitIndex].unitName then
newGroup.units[unitIndex].name = newGroup.units[unitIndex].unitName
elseif newGroup.units[unitIndex].name then
newGroup.units[unitIndex].name = newGroup.units[unitIndex].name
end
end
if newGroup.clone or not unitData.name then
newGroup.units[unitIndex].name = tostring(newGroup.name .. ' unit' .. unitIndex)
end
if not unitData.skill then
newGroup.units[unitIndex].skill = 'Random'
end
if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then
if newGroup.units[unitIndex].alt_type and newGroup.units[unitIndex].alt_type ~= 'BARO' or not newGroup.units[unitIndex].alt_type then
newGroup.units[unitIndex].alt_type = 'RADIO'
end
if not unitData.speed then
if newCat == 'AIRPLANE' then
newGroup.units[unitIndex].speed = 150
elseif newCat == 'HELICOPTER' then
newGroup.units[unitIndex].speed = 60
end
end
if not unitData.payload then
newGroup.units[unitIndex].payload = mist.getPayload(originalName)
end
if not unitData.alt then
if newCat == 'AIRPLANE' then
newGroup.units[unitIndex].alt = 2000
newGroup.units[unitIndex].alt_type = 'RADIO'
newGroup.units[unitIndex].speed = 150
elseif newCat == 'HELICOPTER' then
newGroup.units[unitIndex].alt = 500
newGroup.units[unitIndex].alt_type = 'RADIO'
newGroup.units[unitIndex].speed = 60
end
end
elseif newCat == 'GROUND_UNIT' then
if nil == unitData.playerCanDrive then
unitData.playerCanDrive = true
end
end
mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newGroup.units[unitIndex])
end
mistAddedGroups[#mistAddedGroups + 1] = mist.utils.deepCopy(newGroup)
if newGroup.route then
if newGroup.route and not newGroup.route.points then
if newGroup.route[1] then
local copyRoute = mist.utils.deepCopy(newGroup.route)
newGroup.route = {}
newGroup.route.points = copyRoute
end
end
else -- if aircraft and no route assigned. make a quick and stupid route so AI doesnt RTB immediately
--if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then
newGroup.route = {}
newGroup.route.points = {}
newGroup.route.points[1] = {}
--end
end
newGroup.country = newCountry
-- update and verify any self tasks
if newGroup.route and newGroup.route.points then
--log:warn(newGroup.route.points)
for i, pData in pairs(newGroup.route.points) do
if pData.task and pData.task.params and pData.task.params.tasks and #pData.task.params.tasks > 0 then
for tIndex, tData in pairs(pData.task.params.tasks) do
if tData.params and tData.params.action then
if tData.params.action.id == "EPLRS" then
tData.params.action.params.groupId = newGroup.groupId
elseif tData.params.action.id == "ActivateBeacon" or tData.params.action.id == "ActivateICLS" then
tData.params.action.params.unitId = newGroup.units[1].unitId
end
end
end
end
end
end
--mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, newGroup.name ..'.lua')
--log:warn(newGroup)
-- sanitize table
newGroup.groupName = nil
newGroup.clone = nil
newGroup.category = nil
newGroup.country = nil
newGroup.tasks = {}
for unitIndex, unitData in pairs(newGroup.units) do
newGroup.units[unitIndex].unitName = nil
end
coalition.addGroup(country.id[newCountry], Unit.Category[newCat], newGroup)
return newGroup
end
--- Schedules a function.
-- Modified Slmod task scheduler, superior to timer.scheduleFunction
-- @tparam function f function to schedule
-- @tparam table vars array containing all parameters passed to the function
-- @tparam number t time in seconds from mission start to schedule the function to.
-- @tparam[opt] number rep time between repetitions of the function
-- @tparam[opt] number st time in seconds from mission start at which the function
-- should stop to be rescheduled.
-- @treturn number scheduled function id.
function mist.scheduleFunction(f, vars, t, rep, st)
--verify correct types
assert(type(f) == 'function', 'variable 1, expected function, got ' .. type(f))
assert(type(vars) == 'table' or vars == nil, 'variable 2, expected table or nil, got ' .. type(f))
assert(type(t) == 'number', 'variable 3, expected number, got ' .. type(t))
assert(type(rep) == 'number' or rep == nil, 'variable 4, expected number or nil, got ' .. type(rep))
assert(type(st) == 'number' or st == nil, 'variable 5, expected number or nil, got ' .. type(st))
if not vars then
vars = {}
end
taskId = taskId + 1
table.insert(scheduledTasks, {f = f, vars = vars, t = t, rep = rep, st = st, id = taskId})
return taskId
end
--- Removes a scheduled function.
-- @tparam number id function id
-- @treturn boolean true if function was successfully removed, false otherwise.
function mist.removeFunction(id)
local i = 1
while i <= #scheduledTasks do
if scheduledTasks[i].id == id then
table.remove(scheduledTasks, i)
return true
else
i = i + 1
end
end
return false
end
--- Registers an event handler.
-- @tparam function f function handling event
-- @treturn number id of the event handler
function mist.addEventHandler(f) --id is optional!
local handler = {}
idNum = idNum + 1
handler.id = idNum
handler.f = f
function handler:onEvent(event)
self.f(event)
end
world.addEventHandler(handler)
return handler.id
end
--- Removes event handler with given id.
-- @tparam number id event handler id
-- @treturn boolean true on success, false otherwise
function mist.removeEventHandler(id)
for key, handler in pairs(world.eventHandlers) do
if handler.id and handler.id == id then
world.eventHandlers[key] = nil
return true
end
end
return false
end
end
-- Begin common funcs
do
--- Returns MGRS coordinates as string.
-- @tparam string MGRS MGRS coordinates
-- @tparam number acc the accuracy of each easting/northing.
-- Can be: 0, 1, 2, 3, 4, or 5.
function mist.tostringMGRS(MGRS, acc)
if acc == 0 then
return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph
else
return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Easting/(10^(5-acc)), 0))
.. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Northing/(10^(5-acc)), 0))
end
end
--[[acc:
in DM: decimal point of minutes.
In DMS: decimal point of seconds.
position after the decimal of the least significant digit:
So:
42.32 - acc of 2.
]]
function mist.tostringLL(lat, lon, acc, DMS)
local latHemi, lonHemi
if lat > 0 then
latHemi = 'N'
else
latHemi = 'S'
end
if lon > 0 then
lonHemi = 'E'
else
lonHemi = 'W'
end
lat = math.abs(lat)
lon = math.abs(lon)
local latDeg = math.floor(lat)
local latMin = (lat - latDeg)*60
local lonDeg = math.floor(lon)
local lonMin = (lon - lonDeg)*60
if DMS then -- degrees, minutes, and seconds.
local oldLatMin = latMin
latMin = math.floor(latMin)
local latSec = mist.utils.round((oldLatMin - latMin)*60, acc)
local oldLonMin = lonMin
lonMin = math.floor(lonMin)
local lonSec = mist.utils.round((oldLonMin - lonMin)*60, acc)
if latSec == 60 then
latSec = 0
latMin = latMin + 1
end
if lonSec == 60 then
lonSec = 0
lonMin = lonMin + 1
end
local secFrmtStr -- create the formatting string for the seconds place
if acc <= 0 then -- no decimal place.
secFrmtStr = '%02d'
else
local width = 3 + acc -- 01.310 - that's a width of 6, for example.
secFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
end
return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' '
.. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi
else -- degrees, decimal minutes.
latMin = mist.utils.round(latMin, acc)
lonMin = mist.utils.round(lonMin, acc)
if latMin == 60 then
latMin = 0
latDeg = latDeg + 1
end
if lonMin == 60 then
lonMin = 0
lonDeg = lonDeg + 1
end
local minFrmtStr -- create the formatting string for the minutes place
if acc <= 0 then -- no decimal place.
minFrmtStr = '%02d'
else
local width = 3 + acc -- 01.310 - that's a width of 6, for example.
minFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
end
return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' '
.. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi
end
end
--[[ required: az - radian
required: dist - meters
optional: alt - meters (set to false or nil if you don't want to use it).
optional: metric - set true to get dist and alt in km and m.
precision will always be nearest degree and NM or km.]]
function mist.tostringBR(az, dist, alt, metric)
az = mist.utils.round(mist.utils.toDegree(az), 0)
if metric then
dist = mist.utils.round(dist/1000, 0)
else
dist = mist.utils.round(mist.utils.metersToNM(dist), 0)
end
local s = string.format('%03d', az) .. ' for ' .. dist
if alt then
if metric then
s = s .. ' at ' .. mist.utils.round(alt, 0)
else
s = s .. ' at '
local rounded = mist.utils.round(mist.utils.metersToFeet(alt/1000), 0)
s = s .. rounded
if rounded > 0 then
s = s .. "000"
end
end
end
return s
end
function mist.getNorthCorrection(gPoint) --gets the correction needed for true north
local point = mist.utils.deepCopy(gPoint)
if not point.z then --Vec2; convert to Vec3
point.z = point.y
point.y = 0
end
local lat, lon = coord.LOtoLL(point)
local north_posit = coord.LLtoLO(lat + 1, lon)
return math.atan2(north_posit.z - point.z, north_posit.x - point.x)
end
--- Returns skill of the given unit.
-- @tparam string unitName unit name
-- @return skill of the unit
function mist.getUnitSkill(unitName)
if mist.DBs.unitsByName[unitName] then
if Unit.getByName(unitName) then
local lunit = Unit.getByName(unitName)
local data = mist.DBs.unitsByName[unitName]
if data.unitName == unitName and data.type == lunit:getTypeName() and data.unitId == tonumber(lunit:getID()) and data.skill then
return data.skill
end
end
end
log:error("Unit not found in DB: $1", unitName)
return false
end
--- Returns an array containing a group's units positions.
-- e.g.
-- {
-- [1] = {x = 299435.224, y = -1146632.6773},
-- [2] = {x = 663324.6563, y = 322424.1112}
-- }
-- @tparam number|string groupIdent group id or name
-- @treturn table array containing positions of each group member
function mist.getGroupPoints(groupIdent)
-- search by groupId and allow groupId and groupName as inputs
local gpId = groupIdent
if type(groupIdent) == 'string' and not tonumber(groupIdent) then
if mist.DBs.MEgroupsByName[groupIdent] then
gpId = mist.DBs.MEgroupsByName[groupIdent].groupId
else
log:error("Group not found in mist.DBs.MEgroupsByName: $1", groupIdent)
end
end
for coa_name, coa_data in pairs(env.mission.coalition) do
if type(coa_data) == 'table' then
if coa_data.country then --there is a country table
for cntry_id, cntry_data in pairs(coa_data.country) do
for obj_cat_name, obj_cat_data in pairs(cntry_data) do
if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points
if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group!
for group_num, group_data in pairs(obj_cat_data.group) do
if group_data and group_data.groupId == gpId then -- this is the group we are looking for
if group_data.route and group_data.route.points and #group_data.route.points > 0 then
local points = {}
for point_num, point in pairs(group_data.route.points) do
if not point.point then
points[point_num] = { x = point.x, y = point.y }
else
points[point_num] = point.point --it's possible that the ME could move to the point = Vec2 notation.
end
end
return points
end
return
end --if group_data and group_data.name and group_data.name == 'groupname'
end --for group_num, group_data in pairs(obj_cat_data.group) do
end --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then
end --if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then
end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do
end --for cntry_id, cntry_data in pairs(coa_data.country) do
end --if coa_data.country then --there is a country table
end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then
end --for coa_name, coa_data in pairs(mission.coalition) do
end
--- getUnitAttitude(unit) return values.
-- Yaw, AoA, ClimbAngle - relative to earth reference
-- DOES NOT TAKE INTO ACCOUNT WIND.
-- @table attitude
-- @tfield number Heading in radians, range of 0 to 2*pi,
-- relative to true north.
-- @tfield number Pitch in radians, range of -pi/2 to pi/2
-- @tfield number Roll in radians, range of 0 to 2*pi,
-- right roll is positive direction.
-- @tfield number Yaw in radians, range of -pi to pi,
-- right yaw is positive direction.
-- @tfield number AoA in radians, range of -pi to pi,
-- rotation of aircraft to the right in comparison to
-- flight direction being positive.
-- @tfield number ClimbAngle in radians, range of -pi/2 to pi/2
--- Returns the attitude of a given unit.
-- Will work on any unit, even if not an aircraft.
-- @tparam Unit unit unit whose attitude is returned.
-- @treturn table @{attitude}
function mist.getAttitude(unit)
local unitpos = unit:getPosition()
if unitpos then
local Heading = math.atan2(unitpos.x.z, unitpos.x.x)
Heading = Heading + mist.getNorthCorrection(unitpos.p)
if Heading < 0 then
Heading = Heading + 2*math.pi -- put heading in range of 0 to 2*pi
end
---- heading complete.----
local Pitch = math.asin(unitpos.x.y)
---- pitch complete.----
-- now get roll:
--maybe not the best way to do it, but it works.
--first, make a vector that is perpendicular to y and unitpos.x with cross product
local cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0})
--now, get dot product of of this cross product with unitpos.z
local dp = mist.vec.dp(cp, unitpos.z)
--now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|)
local Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z)))
--now, have to get sign of roll.
-- by convention, making right roll positive
-- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative.
if unitpos.z.y > 0 then -- left roll, flip the sign of the roll
Roll = -Roll
end
---- roll complete. ----
--now, work on yaw, AoA, climb, and abs velocity
local Yaw
local AoA
local ClimbAngle
-- get unit velocity
local unitvel = unit:getVelocity()
if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
local AxialVel = {} --unit velocity transformed into aircraft axes directions
--transform velocity components in direction of aircraft axes.
AxialVel.x = mist.vec.dp(unitpos.x, unitvel)
AxialVel.y = mist.vec.dp(unitpos.y, unitvel)
AxialVel.z = mist.vec.dp(unitpos.z, unitvel)
--Yaw is the angle between unitpos.x and the x and z velocities
--define right yaw as positive
Yaw = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/mist.vec.mag({x = AxialVel.x, y = 0, z = AxialVel.z}))
--now set correct direction:
if AxialVel.z > 0 then
Yaw = -Yaw
end
-- AoA is angle between unitpos.x and the x and y velocities
AoA = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/mist.vec.mag({x = AxialVel.x, y = AxialVel.y, z = 0}))
--now set correct direction:
if AxialVel.y > 0 then
AoA = -AoA
end
ClimbAngle = math.asin(unitvel.y/mist.vec.mag(unitvel))
end
return { Heading = Heading, Pitch = Pitch, Roll = Roll, Yaw = Yaw, AoA = AoA, ClimbAngle = ClimbAngle}
else
log:error("Couldn't get unit's position")
end
end
--- Returns heading of given unit.
-- @tparam Unit unit unit whose heading is returned.
-- @param rawHeading
-- @treturn number heading of the unit, in range
-- of 0 to 2*pi.
function mist.getHeading(unit, rawHeading)
local unitpos = unit:getPosition()
if unitpos then
local Heading = math.atan2(unitpos.x.z, unitpos.x.x)
if not rawHeading then
Heading = Heading + mist.getNorthCorrection(unitpos.p)
end
if Heading < 0 then
Heading = Heading + 2*math.pi -- put heading in range of 0 to 2*pi
end
return Heading
end
end
--- Returns given unit's pitch
-- @tparam Unit unit unit whose pitch is returned.
-- @treturn number pitch of given unit
function mist.getPitch(unit)
local unitpos = unit:getPosition()
if unitpos then
return math.asin(unitpos.x.y)
end
end
--- Returns given unit's roll.
-- @tparam Unit unit unit whose roll is returned.
-- @treturn number roll of given unit
function mist.getRoll(unit)
local unitpos = unit:getPosition()
if unitpos then
-- now get roll:
--maybe not the best way to do it, but it works.
--first, make a vector that is perpendicular to y and unitpos.x with cross product
local cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0})
--now, get dot product of of this cross product with unitpos.z
local dp = mist.vec.dp(cp, unitpos.z)
--now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|)
local Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z)))
--now, have to get sign of roll.
-- by convention, making right roll positive
-- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative.
if unitpos.z.y > 0 then -- left roll, flip the sign of the roll
Roll = -Roll
end
return Roll
end
end
--- Returns given unit's yaw.
-- @tparam Unit unit unit whose yaw is returned.
-- @treturn number yaw of given unit.
function mist.getYaw(unit)
local unitpos = unit:getPosition()
if unitpos then
-- get unit velocity
local unitvel = unit:getVelocity()
if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
local AxialVel = {} --unit velocity transformed into aircraft axes directions
--transform velocity components in direction of aircraft axes.
AxialVel.x = mist.vec.dp(unitpos.x, unitvel)
AxialVel.y = mist.vec.dp(unitpos.y, unitvel)
AxialVel.z = mist.vec.dp(unitpos.z, unitvel)
--Yaw is the angle between unitpos.x and the x and z velocities
--define right yaw as positive
local Yaw = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/mist.vec.mag({x = AxialVel.x, y = 0, z = AxialVel.z}))
--now set correct direction:
if AxialVel.z > 0 then
Yaw = -Yaw
end
return Yaw
end
end
end
--- Returns given unit's angle of attack.
-- @tparam Unit unit unit to get AoA from.
-- @treturn number angle of attack of the given unit.
function mist.getAoA(unit)
local unitpos = unit:getPosition()
if unitpos then
local unitvel = unit:getVelocity()
if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
local AxialVel = {} --unit velocity transformed into aircraft axes directions
--transform velocity components in direction of aircraft axes.
AxialVel.x = mist.vec.dp(unitpos.x, unitvel)
AxialVel.y = mist.vec.dp(unitpos.y, unitvel)
AxialVel.z = mist.vec.dp(unitpos.z, unitvel)
-- AoA is angle between unitpos.x and the x and y velocities
local AoA = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/mist.vec.mag({x = AxialVel.x, y = AxialVel.y, z = 0}))
--now set correct direction:
if AxialVel.y > 0 then
AoA = -AoA
end
return AoA
end
end
end
--- Returns given unit's climb angle.
-- @tparam Unit unit unit to get climb angle from.
-- @treturn number climb angle of given unit.
function mist.getClimbAngle(unit)
local unitpos = unit:getPosition()
if unitpos then
local unitvel = unit:getVelocity()
if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
return math.asin(unitvel.y/mist.vec.mag(unitvel))
end
end
end
--[[--
Unit name table.
Many Mist functions require tables of unit names, which are known
in Mist as UnitNameTables. These follow a special set of shortcuts
borrowed from Slmod. These shortcuts alleviate the problem of entering
huge lists of unit names by hand, and in many cases, they remove the
need to even know the names of the units in the first place!
These are the unit table "short-cut" commands:
Prefixes:
"[-u]<unit name>" - subtract this unit if its in the table
"[g]<group name>" - add this group to the table
"[-g]<group name>" - subtract this group from the table
"[c]<country name>" - add this country's units
"[-c]<country name>" - subtract this country's units if any are in the table
Stand-alone identifiers
"[all]" - add all units
"[-all]" - subtract all units (not very useful by itself)
"[blue]" - add all blue units
"[-blue]" - subtract all blue units
"[red]" - add all red coalition units
"[-red]" - subtract all red units
Compound Identifiers:
"[c][helicopter]<country name>" - add all of this country's helicopters
"[-c][helicopter]<country name>" - subtract all of this country's helicopters
"[c][plane]<country name>" - add all of this country's planes
"[-c][plane]<country name>" - subtract all of this country's planes
"[c][ship]<country name>" - add all of this country's ships
"[-c][ship]<country name>" - subtract all of this country's ships
"[c][vehicle]<country name>" - add all of this country's vehicles
"[-c][vehicle]<country name>" - subtract all of this country's vehicles
"[all][helicopter]" - add all helicopters
"[-all][helicopter]" - subtract all helicopters
"[all][plane]" - add all planes
"[-all][plane]" - subtract all planes
"[all][ship]" - add all ships
"[-all][ship]" - subtract all ships
"[all][vehicle]" - add all vehicles
"[-all][vehicle]" - subtract all vehicles
"[blue][helicopter]" - add all blue coalition helicopters
"[-blue][helicopter]" - subtract all blue coalition helicopters
"[blue][plane]" - add all blue coalition planes
"[-blue][plane]" - subtract all blue coalition planes
"[blue][ship]" - add all blue coalition ships
"[-blue][ship]" - subtract all blue coalition ships
"[blue][vehicle]" - add all blue coalition vehicles
"[-blue][vehicle]" - subtract all blue coalition vehicles
"[red][helicopter]" - add all red coalition helicopters
"[-red][helicopter]" - subtract all red coalition helicopters
"[red][plane]" - add all red coalition planes
"[-red][plane]" - subtract all red coalition planes
"[red][ship]" - add all red coalition ships
"[-red][ship]" - subtract all red coalition ships
"[red][vehicle]" - add all red coalition vehicles
"[-red][vehicle]" - subtract all red coalition vehicles
Country names to be used in [c] and [-c] short-cuts:
Turkey
Norway
The Netherlands
Spain
11
UK
Denmark
USA
Georgia
Germany
Belgium
Canada
France
Israel
Ukraine
Russia
South Ossetia
Abkhazia
Italy
Australia
Austria
Belarus
Bulgaria
Czech Republic
China
Croatia
Finland
Greece
Hungary
India
Iran
Iraq
Japan
Kazakhstan
North Korea
Pakistan
Poland
Romania
Saudi Arabia
Serbia, Slovakia
South Korea
Sweden
Switzerland
Syria
USAF Aggressors
Do NOT use a '[u]' notation for single units. Single units are referenced
the same way as before: Simply input their names as strings.
These unit tables are evaluated in order, and you cannot subtract a unit
from a table before it is added. For example:
{'[blue]', '[-c]Georgia'}
will evaluate to all of blue coalition except those units owned by the
country named "Georgia"; however:
{'[-c]Georgia', '[blue]'}
will evaluate to all of the units in blue coalition, because the addition
of all units owned by blue coalition occurred AFTER the subtraction of all
units owned by Georgia (which actually subtracted nothing at all, since
there were no units in the table when the subtraction occurred).
More examples:
{'[blue][plane]', '[-c]Georgia', '[-g]Hawg 1'}
Evaluates to all blue planes, except those blue units owned by the country
named "Georgia" and the units in the group named "Hawg1".
{'[g]arty1', '[g]arty2', '[-u]arty1_AD', '[-u]arty2_AD', 'Shark 11' }
Evaluates to the unit named "Shark 11", plus all the units in groups named
"arty1" and "arty2" except those that are named "arty1\_AD" and "arty2\_AD".
@table UnitNameTable
]]
--- Returns a table containing unit names.
-- @tparam table tbl sequential strings
-- @treturn table @{UnitNameTable}
function mist.makeUnitTable(tbl, exclude)
--Assumption: will be passed a table of strings, sequential
--log:info(tbl)
local excludeType = {}
if exclude then
if type(exclude) == 'table' then
for x, y in pairs(exclude) do
excludeType[x] = true
excludeType[y] = true
end
else
excludeType[exclude] = true
end
end
local units_by_name = {}
local l_munits = mist.DBs.units --local reference for faster execution
for i = 1, #tbl do
local unit = tbl[i]
if unit:sub(1,4) == '[-u]' then --subtract a unit
if units_by_name[unit:sub(5)] then -- 5 to end
units_by_name[unit:sub(5)] = nil --remove
end
elseif unit:sub(1,3) == '[g]' then -- add a group
for coa, coa_tbl in pairs(l_munits) do
for country, country_table in pairs(coa_tbl) do
for unit_type, unit_type_tbl in pairs(country_table) do
if type(unit_type_tbl) == 'table' then
for group_ind, group_tbl in pairs(unit_type_tbl) do
if type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(4) then
-- index 4 to end
for unit_ind, unit in pairs(group_tbl.units) do
units_by_name[unit.unitName] = true --add
end
end
end
end
end
end
end
elseif unit:sub(1,4) == '[-g]' then -- subtract a group
for coa, coa_tbl in pairs(l_munits) do
for country, country_table in pairs(coa_tbl) do
for unit_type, unit_type_tbl in pairs(country_table) do
if type(unit_type_tbl) == 'table' then
for group_ind, group_tbl in pairs(unit_type_tbl) do
if type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(5) then
-- index 5 to end
for unit_ind, unit in pairs(group_tbl.units) do
if units_by_name[unit.unitName] then
units_by_name[unit.unitName] = nil --remove
end
end
end
end
end
end
end
end
elseif unit:sub(1,3) == '[c]' then -- add a country
local category = ''
local country_start = 4
if unit:sub(4,15) == '[helicopter]' then
category = 'helicopter'
country_start = 16
elseif unit:sub(4,10) == '[plane]' then
category = 'plane'
country_start = 11
elseif unit:sub(4,9) == '[ship]' then
category = 'ship'
country_start = 10
elseif unit:sub(4,12) == '[vehicle]' then
category = 'vehicle'
country_start = 13
elseif unit:sub(4, 11) == '[static]' then
category = 'static'
country_start = 12
end
for coa, coa_tbl in pairs(l_munits) do
for country, country_table in pairs(coa_tbl) do
if country == string.lower(unit:sub(country_start)) then -- match
for unit_type, unit_type_tbl in pairs(country_table) do
if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
for group_ind, group_tbl in pairs(unit_type_tbl) do
if type(group_tbl) == 'table' then
for unit_ind, unit in pairs(group_tbl.units) do
units_by_name[unit.unitName] = true --add
end
end
end
end
end
end
end
end
elseif unit:sub(1,4) == '[-c]' then -- subtract a country
local category = ''
local country_start = 5
if unit:sub(5,16) == '[helicopter]' then
category = 'helicopter'
country_start = 17
elseif unit:sub(5,11) == '[plane]' then
category = 'plane'
country_start = 12
elseif unit:sub(5,10) == '[ship]' then
category = 'ship'
country_start = 11
elseif unit:sub(5,13) == '[vehicle]' then
category = 'vehicle'
country_start = 14
elseif unit:sub(5, 12) == '[static]' then
category = 'static'
country_start = 13
end
for coa, coa_tbl in pairs(l_munits) do
for country, country_table in pairs(coa_tbl) do
if country == string.lower(unit:sub(country_start)) then -- match
for unit_type, unit_type_tbl in pairs(country_table) do
if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
for group_ind, group_tbl in pairs(unit_type_tbl) do
if type(group_tbl) == 'table' then
for unit_ind, unit in pairs(group_tbl.units) do
if units_by_name[unit.unitName] then
units_by_name[unit.unitName] = nil --remove
end
end
end
end
end
end
end
end
end
elseif unit:sub(1,6) == '[blue]' then -- add blue coalition
local category = ''
if unit:sub(7) == '[helicopter]' then
category = 'helicopter'
elseif unit:sub(7) == '[plane]' then
category = 'plane'
elseif unit:sub(7) == '[ship]' then
category = 'ship'
elseif unit:sub(7) == '[vehicle]' then
category = 'vehicle'
elseif unit:sub(7) == '[static]' then
category = 'static'
end
for coa, coa_tbl in pairs(l_munits) do
if coa == 'blue' then
for country, country_table in pairs(coa_tbl) do
for unit_type, unit_type_tbl in pairs(country_table) do
if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
for group_ind, group_tbl in pairs(unit_type_tbl) do
if type(group_tbl) == 'table' then
for unit_ind, unit in pairs(group_tbl.units) do
units_by_name[unit.unitName] = true --add
end
end
end
end
end
end
end
end
elseif unit:sub(1,7) == '[-blue]' then -- subtract blue coalition
local category = ''
if unit:sub(8) == '[helicopter]' then
category = 'helicopter'
elseif unit:sub(8) == '[plane]' then
category = 'plane'
elseif unit:sub(8) == '[ship]' then
category = 'ship'
elseif unit:sub(8) == '[vehicle]' then
category = 'vehicle'
elseif unit:sub(8) == '[static]' then
category = 'static'
end
for coa, coa_tbl in pairs(l_munits) do
if coa == 'blue' then
for country, country_table in pairs(coa_tbl) do
for unit_type, unit_type_tbl in pairs(country_table) do
if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
for group_ind, group_tbl in pairs(unit_type_tbl) do
if type(group_tbl) == 'table' then
for unit_ind, unit in pairs(group_tbl.units) do
if units_by_name[unit.unitName] then
units_by_name[unit.unitName] = nil --remove
end
end
end
end
end
end
end
end
end
elseif unit:sub(1,5) == '[red]' then -- add red coalition
local category = ''
if unit:sub(6) == '[helicopter]' then
category = 'helicopter'
elseif unit:sub(6) == '[plane]' then
category = 'plane'
elseif unit:sub(6) == '[ship]' then
category = 'ship'
elseif unit:sub(6) == '[vehicle]' then
category = 'vehicle'
elseif unit:sub(6) == '[static]' then
category = 'static'
end
for coa, coa_tbl in pairs(l_munits) do
if coa == 'red' then
for country, country_table in pairs(coa_tbl) do
for unit_type, unit_type_tbl in pairs(country_table) do
if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
for group_ind, group_tbl in pairs(unit_type_tbl) do
if type(group_tbl) == 'table' then
for unit_ind, unit in pairs(group_tbl.units) do
units_by_name[unit.unitName] = true --add
end
end
end
end
end
end
end
end
elseif unit:sub(1,6) == '[-red]' then -- subtract red coalition
local category = ''
if unit:sub(7) == '[helicopter]' then
category = 'helicopter'
elseif unit:sub(7) == '[plane]' then
category = 'plane'
elseif unit:sub(7) == '[ship]' then
category = 'ship'
elseif unit:sub(7) == '[vehicle]' then
category = 'vehicle'
elseif unit:sub(7) == '[static]' then
category = 'static'
end
for coa, coa_tbl in pairs(l_munits) do
if coa == 'red' then
for country, country_table in pairs(coa_tbl) do
for unit_type, unit_type_tbl in pairs(country_table) do
if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
for group_ind, group_tbl in pairs(unit_type_tbl) do
if type(group_tbl) == 'table' then
for unit_ind, unit in pairs(group_tbl.units) do
if units_by_name[unit.unitName] then
units_by_name[unit.unitName] = nil --remove
end
end
end
end
end
end
end
end
end
elseif unit:sub(1,5) == '[all]' then -- add all of a certain category (or all categories)
local category = ''
if unit:sub(6) == '[helicopter]' then
category = 'helicopter'
elseif unit:sub(6) == '[plane]' then
category = 'plane'
elseif unit:sub(6) == '[ship]' then
category = 'ship'
elseif unit:sub(6) == '[vehicle]' then
category = 'vehicle'
elseif unit:sub(6) == '[static]' then
category = 'static'
end
for coa, coa_tbl in pairs(l_munits) do
for country, country_table in pairs(coa_tbl) do
for unit_type, unit_type_tbl in pairs(country_table) do
if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
for group_ind, group_tbl in pairs(unit_type_tbl) do
if type(group_tbl) == 'table' then
for unit_ind, unit in pairs(group_tbl.units) do
units_by_name[unit.unitName] = true --add
end
end
end
end
end
end
end
elseif unit:sub(1,6) == '[-all]' then -- subtract all of a certain category (or all categories)
local category = ''
if unit:sub(7) == '[helicopter]' then
category = 'helicopter'
elseif unit:sub(7) == '[plane]' then
category = 'plane'
elseif unit:sub(7) == '[ship]' then
category = 'ship'
elseif unit:sub(7) == '[vehicle]' then
category = 'vehicle'
elseif unit:sub(7) == '[static]' then
category = 'static'
end
for coa, coa_tbl in pairs(l_munits) do
for country, country_table in pairs(coa_tbl) do
for unit_type, unit_type_tbl in pairs(country_table) do
if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
for group_ind, group_tbl in pairs(unit_type_tbl) do
if type(group_tbl) == 'table' then
for unit_ind, unit in pairs(group_tbl.units) do
if units_by_name[unit.unitName] then
units_by_name[unit.unitName] = nil --remove
end
end
end
end
end
end
end
end
else -- just a regular unit
units_by_name[unit] = true --add
end
end
local units_tbl = {} -- indexed sequentially
for unit_name, val in pairs(units_by_name) do
if val then
units_tbl[#units_tbl + 1] = unit_name -- add all the units to the table
end
end
units_tbl.processed = timer.getTime() --add the processed flag
return units_tbl
end
function mist.getUnitsByAttribute(att, rnum, id)
local cEntry = {}
cEntry.type = att.type or att.typeName or att.typename
cEntry.country = att.country
cEntry.coalition = att.coalition
cEntry.skill = att.skill
cEntry.category = att.category
local num = rnum or 1
if cEntry.skill == 'human' then
cEntry.skill = {'Client', 'Player'}
end
local checkedVal = {}
local units = {}
for uName, uData in pairs(mist.DBs.unitsByName) do
local matched = 0
for cName, cVal in pairs(cEntry) do
if type(cVal) == 'table' then
for sName, sVal in pairs(cVal) do
if (uData[cName] and uData[cName] == sVal) or (uData[cName] and uData[cName] == sName) then
matched = matched + 1
end
end
else
if uData[cName] and uData[cName] == cVal then
matched = matched + 1
end
end
end
if matched >= num then
if id then
units[uData.unitId] = true
else
units[uName] = true
end
end
end
local rtn = {}
for name, _ in pairs(units) do
table.insert(rtn, name)
end
return rtn
end
function mist.getGroupsByAttribute(att, rnum, id)
local cEntry = {}
cEntry.type = att.type or att.typeName or att.typename
cEntry.country = att.country
cEntry.coalition = att.coalition
cEntry.skill = att.skill
cEntry.category = att.category
local num = rnum or 1
if cEntry.skill == 'human' then
cEntry.skill = {'Client', 'Player'}
end
local groups = {}
for gName, gData in pairs(mist.DBs.groupsByName) do
local matched = 0
for cName, cVal in pairs(cEntry) do
if type(cVal) == 'table' then
for sName, sVal in pairs(cVal) do
if cName == 'skill' or cName == 'type' then
local lMatch = 0
for uId, uData in pairs(gData.units) do
if (uData[cName] and uData[cName] == sVal) or (gData[cName] and gData[cName] == sName) then
lMatch = lMatch + 1
break
end
end
if lMatch > 0 then
matched = matched + 1
end
end
if (gData[cName] and gData[cName] == sVal) or (gData[cName] and gData[cName] == sName) then
matched = matched + 1
break
end
end
else
if cName == 'skill' or cName == 'type' then
local lMatch = 0
for uId, uData in pairs(gData.units) do
if (uData[cName] and uData[cName] == sVal) then
lMatch = lMatch + 1
break
end
end
if lMatch > 0 then
matched = matched + 1
end
end
if gData[cName] and gData[cName] == cVal then
matched = matched + 1
end
end
end
if matched >= num then
if id then
groups[gData.groupid] = true
else
groups[gName] = true
end
end
end
local rtn = {}
for name, _ in pairs(groups) do
table.insert(rtn, name)
end
return rtn
end
function mist.getDeadMapObjectsFromPoint(p, radius, filters)
local map_objs = {}
local fCheck = filters or {}
local filter = {}
local r = radius or p.radius or 100
local point = mist.utils.makeVec3(p)
local filterSize = 0
for fInd, fVal in pairs(fCheck) do
filterSize = filterSize + 1
filter[string.lower(fInd)] = true
filter[string.lower(fVal)] = true
end
for obj_id, obj in pairs(mist.DBs.deadObjects) do
log:warn(obj)
if obj.objectType and obj.objectType == 'building' then --dead map object
if ((point.x - obj.objectPos.x)^2 + (point.z - obj.objectPos.z)^2)^0.5 <= r then
if filterSize == 0 or (obj.typeName and filter[string.lower(obj.typeName)])then
map_objs[#map_objs + 1] = mist.utils.deepCopy(obj)
end
end
end
end
return map_objs
end
function mist.getDeadMapObjsInZones(zone_names, filters)
-- zone_names: table of zone names
-- returns: table of dead map objects (indexed numerically)
local map_objs = {}
local zones = {}
for i = 1, #zone_names do
if mist.DBs.zonesByName[zone_names[i]] then
zones[#zones + 1] = mist.DBs.zonesByName[zone_names[i]]
end
end
for i = 1, #zones do
local rtn = mist.getDeadMapObjectsFromPoint(zones[i], nil, filters)
for j = 1, #rtn do
map_objs[#map_objs + 1] = rtn[j]
end
end
return map_objs
end
function mist.getDeadMapObjsInPolygonZone(zone, filters)
-- zone_names: table of zone names
-- returns: table of dead map objects (indexed numerically)
local filter = {}
local fCheck = filters or {}
local filterSize = 0
for fInd, fVal in pairs(fCheck) do
filterSize = filterSize + 1
filter[string.lower(fInd)] = true
filter[string.lower(fVal)] = true
end
local map_objs = {}
for obj_id, obj in pairs(mist.DBs.deadObjects) do
if obj.objectType and obj.objectType == 'building' then --dead map object
if mist.pointInPolygon(obj.objectPos, zone) and (filterSize == 0 or filter[string.lower(obj.objectData.type)]) then
map_objs[#map_objs + 1] = mist.utils.deepCopy(obj)
end
end
end
return map_objs
end
mist.shape = {}
function mist.shape.insideShape(shape1, shape2, full)
if shape1.radius then -- probably a circle
if shape2.radius then
return mist.shape.circleInCircle(shape1, shape2, full)
elseif shape2[1] then
return mist.shape.circleInPoly(shape1, shape2, full)
end
elseif shape1[1] then -- shape1 is probably a polygon
if shape2.radius then
return mist.shape.polyInCircle(shape1, shape2, full)
elseif shape2[1] then
return mist.shape.polyInPoly(shape1, shape2, full)
end
end
return false
end
function mist.shape.circleInCircle(c1, c2, full)
if not full then -- quick partial check
if mist.utils.get2DDist(c1.point, c2.point) <= c2.radius then
return true
end
end
local theta = mist.utils.getHeadingPoints(c2.point, c1.point) -- heading from
if full then
return mist.utils.get2DDist(mist.projectPoint(c1.point, c1.radius, theta), c2.point) <= c2.radius
else
return mist.utils.get2DDist(mist.projectPoint(c1.point, c1.radius, theta + math.pi), c2.point) <= c2.radius
end
return false
end
function mist.shape.circleInPoly(circle, poly, full)
if poly and type(poly) == 'table' and circle and type(circle) == 'table' and circle.radius and circle.point then
if not full then
for i = 1, #poly do
if mist.utils.get2DDist(circle.point, poly[i]) <= circle.radius then
return true
end
end
end
-- no point is inside of the zone, now check if any part is
local count = 0
for i = 1, #poly do
local theta -- heading of each set of points
if i == #poly then
theta = mist.utils.getHeadingPoints(poly[i],poly[1])
else
theta = mist.utils.getHeadingPoints(poly[i],poly[i+1])
end
-- offset
local pPoint = mist.projectPoint(circle.point, circle.radius, theta - (math.pi/180))
local oPoint = mist.projectPoint(circle.point, circle.radius, theta + (math.pi/180))
if mist.pointInPolygon(pPoint, poly) == true then
if (full and mist.pointInPolygon(oPoint, poly) == true) or not full then
return true
end
end
end
end
return false
end
function mist.shape.polyInPoly(p1, p2, full)
local count = 0
for i = 1, #p1 do
if mist.pointInPolygon(p1[i], p2) then
count = count + 1
end
if (not full) and count > 0 then
return true
end
end
if count == #p1 then
return true
end
return false
end
function mist.shape.polyInCircle(poly, circle, full)
local count = 0
for i = 1, #poly do
if mist.utils.get2DDist(circle.point, poly[i]) <= circle.radius then
if full then
count = count + 1
else
return true
end
end
end
if count == #poly then
return true
end
return false
end
function mist.shape.getPointOnSegment(point, seg, isSeg)
local p = mist.utils.makeVec2(point)
local s1 = mist.utils.makeVec2(seg[1])
local s2 = mist.utils.makeVec2(seg[2])
local cx, cy = p.x - s1.x, p.y - s1.y
local dx, dy = s2.x - s1.x, s2.y - s1.y
local d = (dx*dx + dy*dy)
if d == 0 then
return {x = s1.x, y = s1.y}
end
local u = (cx*dx + cy*dy)/d
if isSeg then
if u < 0 then
u = 0
elseif u > 1 then
u = 1
end
end
return {x = s1.x + u*dx, y = s1.y + u*dy}
end
function mist.shape.segmentIntersect(seg1, seg2)
local segA = {mist.utils.makeVec2(seg1[1]), mist.utils.makeVec2(seg1[2])}
local segB = {mist.utils.makeVec2(seg2[1]), mist.utils.makeVec2(seg2[2])}
local dx1, dy1 = segA[2].x - segA[1].x, segA[2].y - segA[1].y
local dx2, dy2 = segB[2].x - segB[1].x, segB[2].y - segB[1].y
local dx3, dy3 = segA[1].x - segB[1].x, segA[1].y - segB[1].y
local d = dx1*dy2 - dy1*dx2
if d == 0 then
return false
end
local t1 = (dx2*dy3 - dy2*dx3)/d
if t1 < 0 or t1 > 1 then
return false
end
local t2 = (dx1*dy3 - dy1*dx3)/d
if t2 < 0 or t2 > 1 then
return false
end
-- point of intersection
return true, {x = segA[1].x + t1*dx1, y = segA[1].y + t1*dy1}
end
function mist.pointInPolygon(point, poly, maxalt) --raycasting point in polygon. Code from http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm
--[[local type_tbl = {
point = {'table'},
poly = {'table'},
maxalt = {'number', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.pointInPolygon', type_tbl, {point, poly, maxalt})
assert(err, errmsg)
]]
point = mist.utils.makeVec3(point)
local px = point.x
local pz = point.z
local cn = 0
local newpoly = mist.utils.deepCopy(poly)
if not maxalt or (point.y <= maxalt) then
local polysize = #newpoly
newpoly[#newpoly + 1] = newpoly[1]
newpoly[1] = mist.utils.makeVec3(newpoly[1])
for k = 1, polysize do
newpoly[k+1] = mist.utils.makeVec3(newpoly[k+1])
if ((newpoly[k].z <= pz) and (newpoly[k+1].z > pz)) or ((newpoly[k].z > pz) and (newpoly[k+1].z <= pz)) then
local vt = (pz - newpoly[k].z) / (newpoly[k+1].z - newpoly[k].z)
if (px < newpoly[k].x + vt*(newpoly[k+1].x - newpoly[k].x)) then
cn = cn + 1
end
end
end
return cn%2 == 1
else
return false
end
end
function mist.mapValue(val, inMin, inMax, outMin, outMax)
return (val - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
end
function mist.getUnitsInPolygon(unit_names, polyZone, max_alt)
local units = {}
for i = 1, #unit_names do
units[#units + 1] = Unit.getByName(unit_names[i]) or StaticObject.getByName(unit_names[i])
end
local inZoneUnits = {}
for i =1, #units do
local lUnit = units[i]
local lCat = Object.getCategory(lUnit)
if lUnit:isExist() == true and ((lCat == 1 and lUnit:isActive()) or lCat ~= 1) and mist.pointInPolygon(lUnit:getPosition().p, polyZone, max_alt) then
inZoneUnits[#inZoneUnits + 1] = lUnit
end
end
return inZoneUnits
end
function mist.getUnitsInZones(unit_names, zone_names, zone_type)
zone_type = zone_type or 'cylinder'
if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then
zone_type = 'cylinder'
end
if zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then
zone_type = 'sphere'
end
assert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type))
local units = {}
local zones = {}
if zone_names and type(zone_names) == 'string' then
zone_names = {zone_names}
end
for k = 1, #unit_names do
local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k])
if unit and unit:isExist() == true then
units[#units + 1] = unit
end
end
for k = 1, #zone_names do
local zone = mist.DBs.zonesByName[zone_names[k]]
if zone then
zones[#zones + 1] = {radius = zone.radius, x = zone.point.x, y = zone.point.y, z = zone.point.z, verts = zone.verticies}
end
end
local in_zone_units = {}
for units_ind = 1, #units do
local lUnit = units[units_ind]
local unit_pos = lUnit:getPosition().p
local lCat = Object.getCategory(lUnit)
for zones_ind = 1, #zones do
if zone_type == 'sphere' then --add land height value for sphere zone type
local alt = land.getHeight({x = zones[zones_ind].x, y = zones[zones_ind].z})
if alt then
zones[zones_ind].y = alt
end
end
if unit_pos and ((lCat == 1 and lUnit:isActive() == true) or lCat ~= 1) then -- it is a unit and is active or it is not a unit
if zones[zones_ind].verts then
if mist.pointInPolygon(unit_pos, zones[zones_ind].verts) then
in_zone_units[#in_zone_units + 1] = lUnit
end
else
if zone_type == 'cylinder' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then
in_zone_units[#in_zone_units + 1] = lUnit
break
elseif zone_type == 'sphere' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.y - zones[zones_ind].y)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then
in_zone_units[#in_zone_units + 1] = lUnit
break
end
end
end
end
end
return in_zone_units
end
function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_type)
zone_type = zone_type or 'cylinder'
if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then
zone_type = 'cylinder'
end
if zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then
zone_type = 'sphere'
end
assert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type))
local units = {}
local zone_units = {}
for k = 1, #unit_names do
local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k])
if unit and unit:isExist() == true then
units[#units + 1] = unit
end
end
for k = 1, #zone_unit_names do
local unit = Unit.getByName(zone_unit_names[k]) or StaticObject.getByName(zone_unit_names[k])
if unit and unit:isExist() == true then
zone_units[#zone_units + 1] = unit
end
end
local in_zone_units = {}
for units_ind = 1, #units do
local lUnit = units[units_ind]
local lCat = Object.getCategory(lUnit)
local unit_pos = lUnit:getPosition().p
for zone_units_ind = 1, #zone_units do
local zone_unit_pos = zone_units[zone_units_ind]:getPosition().p
if unit_pos and zone_unit_pos and ((lCat == 1 and lUnit:isActive()) or lCat ~= 1) then
if zone_type == 'cylinder' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then
in_zone_units[#in_zone_units + 1] = lUnit
break
elseif zone_type == 'sphere' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.y - zone_unit_pos.y)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then
in_zone_units[#in_zone_units + 1] = lUnit
break
end
end
end
end
return in_zone_units
end
function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)
--log:info("$1, $2, $3, $4, $5", unitset1, altoffset1, unitset2, altoffset2, radius)
radius = radius or math.huge
local unit_info1 = {}
local unit_info2 = {}
-- get the positions all in one step, saves execution time.
for unitset1_ind = 1, #unitset1 do
local unit1 = Unit.getByName(unitset1[unitset1_ind])
if unit1 then
local lCat = Object.getCategory(unit1)
if ((lCat == 1 and unit1:isActive()) or lCat ~= 1) and unit1:isExist() == true then
unit_info1[#unit_info1 + 1] = {}
unit_info1[#unit_info1].unit = unit1
unit_info1[#unit_info1].pos = unit1:getPosition().p
end
end
end
for unitset2_ind = 1, #unitset2 do
local unit2 = Unit.getByName(unitset2[unitset2_ind])
if unit2 then
local lCat = Object.getCategory(unit2)
if ((lCat == 1 and unit2:isActive()) or lCat ~= 1) and unit2:isExist() == true then
unit_info2[#unit_info2 + 1] = {}
unit_info2[#unit_info2].unit = unit2
unit_info2[#unit_info2].pos = unit2:getPosition().p
end
end
end
local LOS_data = {}
-- now compute los
for unit1_ind = 1, #unit_info1 do
local unit_added = false
for unit2_ind = 1, #unit_info2 do
if radius == math.huge or (mist.vec.mag(mist.vec.sub(unit_info1[unit1_ind].pos, unit_info2[unit2_ind].pos)) < radius) then -- inside radius
local point1 = { x = unit_info1[unit1_ind].pos.x, y = unit_info1[unit1_ind].pos.y + altoffset1, z = unit_info1[unit1_ind].pos.z}
local point2 = { x = unit_info2[unit2_ind].pos.x, y = unit_info2[unit2_ind].pos.y + altoffset2, z = unit_info2[unit2_ind].pos.z}
if land.isVisible(point1, point2) then
if unit_added == false then
unit_added = true
LOS_data[#LOS_data + 1] = {}
LOS_data[#LOS_data].unit = unit_info1[unit1_ind].unit
LOS_data[#LOS_data].vis = {}
LOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit
else
LOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit
end
end
end
end
end
return LOS_data
end
function mist.getAvgPoint(points)
local avgX, avgY, avgZ, totNum = 0, 0, 0, 0
for i = 1, #points do
--log:warn(points[i])
local nPoint = mist.utils.makeVec3(points[i])
if nPoint.z then
avgX = avgX + nPoint.x
avgY = avgY + nPoint.y
avgZ = avgZ + nPoint.z
totNum = totNum + 1
end
end
if totNum ~= 0 then
return {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum}
end
end
--Gets the average position of a group of units (by name)
function mist.getAvgPos(unitNames)
local avgX, avgY, avgZ, totNum = 0, 0, 0, 0
for i = 1, #unitNames do
local unit
if Unit.getByName(unitNames[i]) then
unit = Unit.getByName(unitNames[i])
elseif StaticObject.getByName(unitNames[i]) then
unit = StaticObject.getByName(unitNames[i])
end
if unit and unit:isExist() == true then
local pos = unit:getPosition().p
if pos then -- you never know O.o
avgX = avgX + pos.x
avgY = avgY + pos.y
avgZ = avgZ + pos.z
totNum = totNum + 1
end
end
end
if totNum ~= 0 then
return {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum}
end
end
function mist.getAvgGroupPos(groupName)
if type(groupName) == 'string' and Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then
groupName = Group.getByName(groupName)
end
local units = {}
for i = 1, groupName:getSize() do
table.insert(units, groupName:getUnit(i):getName())
end
return mist.getAvgPos(units)
end
--[[ vars for mist.getMGRSString:
vars.units - table of unit names (NOT unitNameTable- maybe this should change).
vars.acc - integer between 0 and 5, inclusive
]]
function mist.getMGRSString(vars)
local units = vars.units
local acc = vars.acc or 5
local avgPos = mist.getAvgPos(units)
if avgPos then
return mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc)
end
end
--[[ vars for mist.getLLString
vars.units - table of unit names (NOT unitNameTable- maybe this should change).
vars.acc - integer, number of numbers after decimal place
vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes.
]]
function mist.getLLString(vars)
local units = vars.units
local acc = vars.acc or 3
local DMS = vars.DMS
local avgPos = mist.getAvgPos(units)
if avgPos then
local lat, lon = coord.LOtoLL(avgPos)
return mist.tostringLL(lat, lon, acc, DMS)
end
end
--[[
vars.units- table of unit names (NOT unitNameTable- maybe this should change).
vars.ref - vec3 ref point, maybe overload for vec2 as well?
vars.alt - boolean, if used, includes altitude in string
vars.metric - boolean, gives distance in km instead of NM.
]]
function mist.getBRString(vars)
local units = vars.units
local ref = mist.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already.
local alt = vars.alt
local metric = vars.metric
local avgPos = mist.getAvgPos(units)
if avgPos then
local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z}
local dir = mist.utils.getDir(vec, ref)
local dist = mist.utils.get2DDist(avgPos, ref)
if alt then
alt = avgPos.y
end
return mist.tostringBR(dir, dist, alt, metric)
end
end
-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction.
--[[ vars for mist.getLeadingPos:
vars.units - table of unit names
vars.heading - direction
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
]]
function mist.getLeadingPos(vars)
local units = vars.units
local heading = vars.heading
local radius = vars.radius
if vars.headingDegrees then
heading = mist.utils.toRadian(vars.headingDegrees)
end
local unitPosTbl = {}
for i = 1, #units do
local unit = Unit.getByName(units[i])
if unit and unit:isExist() then
unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p
end
end
if #unitPosTbl > 0 then -- one more more units found.
-- first, find the unit most in the heading direction
local maxPos = -math.huge
heading = heading * -1 -- rotated value appears to be opposite of what was expected
local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd =
for i = 1, #unitPosTbl do
local rotatedVec2 = mist.vec.rotateVec2(mist.utils.makeVec2(unitPosTbl[i]), heading)
if (not maxPos) or maxPos < rotatedVec2.x then
maxPos = rotatedVec2.x
maxPosInd = i
end
end
--now, get all the units around this unit...
local avgPos
if radius then
local maxUnitPos = unitPosTbl[maxPosInd]
local avgx, avgy, avgz, totNum = 0, 0, 0, 0
for i = 1, #unitPosTbl do
if mist.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then
avgx = avgx + unitPosTbl[i].x
avgy = avgy + unitPosTbl[i].y
avgz = avgz + unitPosTbl[i].z
totNum = totNum + 1
end
end
avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum}
else
avgPos = unitPosTbl[maxPosInd]
end
return avgPos
end
end
--[[ vars for mist.getLeadingMGRSString:
vars.units - table of unit names
vars.heading - direction
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
vars.acc - number, 0 to 5.
]]
function mist.getLeadingMGRSString(vars)
local pos = mist.getLeadingPos(vars)
if pos then
local acc = vars.acc or 5
return mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc)
end
end
--[[ vars for mist.getLeadingLLString:
vars.units - table of unit names
vars.heading - direction, number
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
vars.acc - number of digits after decimal point (can be negative)
vars.DMS - boolean, true if you want DMS.
]]
function mist.getLeadingLLString(vars)
local pos = mist.getLeadingPos(vars)
if pos then
local acc = vars.acc or 3
local DMS = vars.DMS
local lat, lon = coord.LOtoLL(pos)
return mist.tostringLL(lat, lon, acc, DMS)
end
end
--[[ vars for mist.getLeadingBRString:
vars.units - table of unit names
vars.heading - direction, number
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
vars.metric - boolean, if true, use km instead of NM.
vars.alt - boolean, if true, include altitude.
vars.ref - vec3/vec2 reference point.
]]
function mist.getLeadingBRString(vars)
local pos = mist.getLeadingPos(vars)
if pos then
local ref = vars.ref
local alt = vars.alt
local metric = vars.metric
local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z}
local dir = mist.utils.getDir(vec, ref)
local dist = mist.utils.get2DDist(pos, ref)
if alt then
alt = pos.y
end
return mist.tostringBR(dir, dist, alt, metric)
end
end
--[[getPathLength from GSH
-- Returns the length between the defined set of points. Can also return the point index before the cutoff was achieved
p - table of path points, vec2 or vec3
cutoff - number distance after which to stop at
topo - boolean for if it should get the topographical distance
]]
function mist.getPathLength(p, cutoff, topo)
local l = 0
local cut = 0 or cutOff
local path = {}
for i = 1, #p do
if topo then
table.insert(path, mist.utils.makeVec3GL(p[i]))
else
table.insert(path, mist.utils.makeVec3(p[i]))
end
end
for i = 1, #path do
if i + 1 <= #path then
if topo then
l = mist.utils.get3DDist(path[i], path[i+1]) + l
else
l = mist.utils.get2DDist(path[i], path[i+1]) + l
end
end
if cut ~= 0 and l > cut then
return l, i
end
end
return l
end
--[[
Return a series of points to simplify the input table. Best used in conjunction with findPathOnRoads to turn the massive table into a list of X points.
p - table of path points, can be vec2 or vec3
num - number of segments.
exact - boolean for whether or not it returns the exact distance or uses the first WP to that distance.
]]
function mist.getPathInSegments(p, num, exact)
local tot = mist.getPathLength(p)
local checkDist = tot/num
local typeUsed = 'vec2'
local points = {[1] = p[1]}
local curDist = 0
for i = 1, #p do
if i + 1 <= #p then
curDist = mist.utils.get2DDist(p[i], p[i+1]) + curDist
if curDist > checkDist then
curDist = 0
if exact then
-- get avg point between the two
-- insert into point table
-- need to be accurate... maybe reassign the point for the value it is checking?
-- insert into p table?
else
table.insert(points, p[i])
end
end
end
end
return points
end
function mist.getPointAtDistanceOnPath(p, dist, r, rtn)
log:info('find distance: $1', dist)
local rType = r or 'roads'
local point = {x= 0, y = 0, z = 0}
local path = {}
local ret = rtn or 'vec2'
local l = 0
if p[1] and #p == 2 then
path = land.findPathOnRoads(rType, p[1].x, p[1].y, p[2].x, p[2].y)
else
path = p
end
for i = 1, #path do
if i + 1 <= #path then
nextPoint = path[i+1]
if topo then
l = mist.utils.get3DDist(path[i], path[i+1]) + l
else
l = mist.utils.get2DDist(path[i], path[i+1]) + l
end
end
if l > dist then
local diff = dist
if i ~= 1 then -- get difference
diff = l - dist
end
local dir = mist.utils.getHeadingPoints(mist.utils.makeVec3(path[i]), mist.utils.makeVec3(path[i+1]))
local x, y
if r then
x, y = land.getClosestPointOnRoads(rType, mist.utils.round((math.cos(dir) * diff) + path[i].x,1), mist.utils.round((math.sin(dir) * diff) + path[i].y,1))
else
x, y = mist.utils.round((math.cos(dir) * diff) + path[i].x,1), mist.utils.round((math.sin(dir) * diff) + path[i].y,1)
end
if ret == 'vec2' then
return {x = x, y = y}, dir
elseif ret == 'vec3' then
return {x = x, y = 0, z = y}, dir
end
return {x = x, y = y}, dir
end
end
log:warn('Find point at distance: $1, path distance $2', dist, l)
return false
end
function mist.projectPoint(point, dist, theta)
local newPoint = {}
if point.z then
newPoint.z = mist.utils.round(math.sin(theta) * dist + point.z, 3)
newPoint.y = mist.utils.deepCopy(point.y)
else
newPoint.y = mist.utils.round(math.sin(theta) * dist + point.y, 3)
end
newPoint.x = mist.utils.round(math.cos(theta) * dist + point.x, 3)
return newPoint
end
end
--- Group functions.
-- @section groups
do -- group functions scope
--- Check table used for group creation.
-- @tparam table groupData table to check.
-- @treturn boolean true if a group can be spawned using
-- this table, false otherwise.
function mist.groupTableCheck(groupData)
-- return false if country, category
-- or units are missing
if not groupData.country or
not groupData.category or
not groupData.units then
return false
end
-- return false if unitData misses
-- x, y or type
for unitId, unitData in pairs(groupData.units) do
if not unitData.x or
not unitData.y or
not unitData.type then
return false
end
end
-- everything we need is here return true
return true
end
--- Returns group data table of give group.
function mist.getCurrentGroupData(gpName)
local dbData = mist.getGroupData(gpName) or {}
if Group.getByName(gpName) and Group.getByName(gpName):isExist() == true then
local newGroup = Group.getByName(gpName)
local newData = mist.utils.deepCopy(dbData)
newData.name = gpName
newData.groupId = tonumber(newGroup:getID())
newData.category = newGroup:getCategory()
newData.groupName = gpName
newData.hidden = dbData.hidden
if newData.category == 2 then
newData.category = 'vehicle'
elseif newData.category == 3 then
newData.category = 'ship'
end
newData.units = {}
local newUnits = newGroup:getUnits()
if #newUnits == 0 then
log:warn('getCurrentGroupData has returned no units for: $1', gpName)
end
for unitNum, unitData in pairs(newGroup:getUnits()) do
newData.units[unitNum] = {}
local uName = unitData:getName()
if mist.DBs.unitsByName[uName] and unitData:getTypeName() == mist.DBs.unitsByName[uName].type and mist.DBs.unitsByName[uName].unitId == tonumber(unitData:getID()) then -- If old data matches most of new data
newData.units[unitNum] = mist.utils.deepCopy(mist.DBs.unitsByName[uName])
else
newData.units[unitNum].unitId = tonumber(unitData:getID())
newData.units[unitNum].type = unitData:getTypeName()
newData.units[unitNum].skill = mist.getUnitSkill(uName)
newData.country = string.lower(country.name[unitData:getCountry()])
newData.units[unitNum].callsign = unitData:getCallsign()
newData.units[unitNum].unitName = uName
end
local pos = unitData:getPosition()
newData.units[unitNum].x = pos.p.x
newData.units[unitNum].y = pos.p.z
newData.units[unitNum].point = {x = newData.units[unitNum].x, y = newData.units[unitNum].y}
newData.units[unitNum].heading = math.atan2(pos.x.z, pos.x.x)
newData.units[unitNum].alt = pos.p.y
newData.units[unitNum].speed = mist.vec.mag(unitData:getVelocity())
end
return newData
elseif StaticObject.getByName(gpName) and StaticObject.getByName(gpName):isExist() == true and dbData.units then
local staticObj = StaticObject.getByName(gpName)
local pos =staticObj:getPosition()
dbData.units[1].x = pos.p.x
dbData.units[1].y = pos.p.z
dbData.units[1].alt = pos.p.y
dbData.units[1].heading = math.atan2(pos.x.z, pos.x.x)
return dbData
end
end
function mist.getGroupData(gpName, route)
local found = false
local newData = {}
if mist.DBs.groupsByName[gpName] then
newData = mist.utils.deepCopy(mist.DBs.groupsByName[gpName])
found = true
end
if found == false then
for groupName, groupData in pairs(mist.DBs.groupsByName) do
if mist.stringMatch(groupName, gpName) == true then
newData = mist.utils.deepCopy(groupData)
newData.groupName = groupName
found = true
break
end
end
end
local payloads
if newData.category == 'plane' or newData.category == 'helicopter' then
payloads = mist.getGroupPayload(newData.groupName)
end
if found == true then
--newData.hidden = false -- maybe add this to DBs
for unitNum, unitData in pairs(newData.units) do
newData.units[unitNum] = {}
newData.units[unitNum].unitId = unitData.unitId
--newData.units[unitNum].point = unitData.point
newData.units[unitNum].x = unitData.point.x
newData.units[unitNum].y = unitData.point.y
newData.units[unitNum].alt = unitData.alt
newData.units[unitNum].alt_type = unitData.alt_type
newData.units[unitNum].speed = unitData.speed
newData.units[unitNum].type = unitData.type
newData.units[unitNum].skill = unitData.skill
newData.units[unitNum].unitName = unitData.unitName
newData.units[unitNum].heading = unitData.heading -- added to DBs
newData.units[unitNum].playerCanDrive = unitData.playerCanDrive -- added to DBs
newData.units[unitNum].livery_id = unitData.livery_id
newData.units[unitNum].AddPropAircraft = unitData.AddPropAircraft
newData.units[unitNum].AddPropVehicle = unitData.AddPropVehicle
if newData.category == 'plane' or newData.category == 'helicopter' then
newData.units[unitNum].payload = payloads[unitNum]
newData.units[unitNum].onboard_num = unitData.onboard_num
newData.units[unitNum].callsign = unitData.callsign
end
if newData.category == 'static' then
newData.units[unitNum].categoryStatic = unitData.categoryStatic
newData.units[unitNum].mass = unitData.mass
newData.units[unitNum].canCargo = unitData.canCargo
newData.units[unitNum].shape_name = unitData.shape_name
end
end
--log:info(newData)
if route then
newData.route = mist.getGroupRoute(gpName, true)
end
return newData
else
log:error('$1 not found in MIST database', gpName)
return
end
end
function mist.getPayload(unitIdent)
-- refactor to search by groupId and allow groupId and groupName as inputs
local unitId = unitIdent
if type(unitIdent) == 'string' and not tonumber(unitIdent) then
if mist.DBs.MEunitsByName[unitIdent] then
unitId = mist.DBs.MEunitsByName[unitIdent].unitId
else
log:error("Unit not found in mist.DBs.MEunitsByName: $1", unitIdent)
return {}
end
elseif type(unitIdent) == "number" and not mist.DBs.MEunitsById[unitIdent] then
log:error("Unit not found in mist.DBs.MEunitsBId: $1", unitIdent)
return {}
end
local ref = mist.DBs.MEunitsById[unitId]
if ref then
local gpId = mist.DBs.MEunitsById[unitId].groupId
if gpId and unitId then
for coa_name, coa_data in pairs(env.mission.coalition) do
if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then
if coa_data.country then --there is a country table
for cntry_id, cntry_data in pairs(coa_data.country) do
for obj_cat_name, obj_cat_data in pairs(cntry_data) do
if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points
if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group!
for group_num, group_data in pairs(obj_cat_data.group) do
if group_data and group_data.groupId == gpId then
for unitIndex, unitData in pairs(group_data.units) do --group index
if unitData.unitId == unitId then
return unitData.payload
end
end
end
end
end
end
end
end
end
end
end
end
else
log:error('Need string or number. Got: $1', type(unitIdent))
return {}
end
log:warn("Couldn't find payload for unit: $1", unitIdent)
return {}
end
function mist.getGroupPayload(groupIdent)
local gpId = groupIdent
if type(groupIdent) == 'string' and not tonumber(groupIdent) then
if mist.DBs.MEgroupsByName[groupIdent] then
gpId = mist.DBs.MEgroupsByName[groupIdent].groupId
else
log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent)
return {}
end
end
if gpId then
for coa_name, coa_data in pairs(env.mission.coalition) do
if type(coa_data) == 'table' then
if coa_data.country then --there is a country table
for cntry_id, cntry_data in pairs(coa_data.country) do
for obj_cat_name, obj_cat_data in pairs(cntry_data) do
if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points
if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group!
for group_num, group_data in pairs(obj_cat_data.group) do
if group_data and group_data.groupId == gpId then
local payloads = {}
for unitIndex, unitData in pairs(group_data.units) do --group index
payloads[unitIndex] = unitData.payload
end
return payloads
end
end
end
end
end
end
end
end
end
else
log:error('Need string or number. Got: $1', type(groupIdent))
return {}
end
log:warn("Couldn't find payload for group: $1", groupIdent)
return {}
end
function mist.getGroupTable(groupIdent)
local gpId = groupIdent
if type(groupIdent) == 'string' and not tonumber(groupIdent) then
if mist.DBs.MEgroupsByName[groupIdent] then
gpId = mist.DBs.MEgroupsByName[groupIdent].groupId
else
log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent)
end
end
if gpId then
for coa_name, coa_data in pairs(env.mission.coalition) do
if type(coa_data) == 'table' then
if coa_data.country then --there is a country table
for cntry_id, cntry_data in pairs(coa_data.country) do
for obj_cat_name, obj_cat_data in pairs(cntry_data) do
if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points
if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group!
for group_num, group_data in pairs(obj_cat_data.group) do
if group_data and group_data.groupId == gpId then
local gp = mist.utils.deepCopy(group_data)
gp.category = obj_cat_name
gp.country = cntry_data.id
return gp
end
end
end
end
end
end
end
end
end
else
log:error('Need string or number. Got: $1', type(groupIdent))
return false
end
log:warn("Couldn't find table for group: $1", groupIdent)
end
function mist.getValidRandomPoint(vars)
end
function mist.teleportToPoint(vars) -- main teleport function that all of teleport/respawn functions call
--log:warn(vars)
local point = vars.point
local gpName
if vars.gpName then
gpName = vars.gpName
elseif vars.groupName then
gpName = vars.groupName
else
log:error('Missing field groupName or gpName in variable table. Table: $1', vars)
end
--[[New vars to add, mostly for when called via inZone functions
anyTerrain
offsetWP1
offsetRoute
initTasks
]]
local action = vars.action
local disperse = vars.disperse or false
local maxDisp = vars.maxDisp or 200
local radius = vars.radius or 0
local innerRadius = vars.innerRadius
local dbData = false
local newGroupData
if gpName and not vars.groupData then
if string.lower(action) == 'teleport' or string.lower(action) == 'tele' then
newGroupData = mist.getCurrentGroupData(gpName)
elseif string.lower(action) == 'respawn' then
newGroupData = mist.getGroupData(gpName)
dbData = true
elseif string.lower(action) == 'clone' then
newGroupData = mist.getGroupData(gpName)
newGroupData.clone = 'order66'
dbData = true
else
action = 'tele'
newGroupData = mist.getCurrentGroupData(gpName)
end
else
action = 'tele'
newGroupData = vars.groupData
end
if vars.newGroupName then
newGroupData.groupName = vars.newGroupName
end
if #newGroupData.units == 0 then
log:warn('$1 has no units in group table', gpName)
return
end
--log:info('get Randomized Point')
local diff = {x = 0, y = 0}
local newCoord, origCoord
local validTerrain = {'LAND', 'ROAD', 'SHALLOW_WATER', 'WATER', 'RUNWAY'}
if vars.anyTerrain then
-- do nothing
elseif vars.validTerrain then
validTerrain = vars.validTerrain
else
if string.lower(newGroupData.category) == 'ship' then
validTerrain = {'SHALLOW_WATER' , 'WATER'}
elseif string.lower(newGroupData.category) == 'vehicle' then
validTerrain = {'LAND', 'ROAD'}
end
end
if point and radius >= 0 then
local valid = false
-- new thoughts
--[[ Get AVG position of group and max radius distance to that avg point, otherwise use disperse data to get zone area to check
if disperse then
else
end
-- ]]
---- old
for i = 1, 100 do
newCoord = mist.getRandPointInCircle(point, radius, innerRadius)
if vars.anyTerrain or mist.isTerrainValid(newCoord, validTerrain) then
origCoord = mist.utils.deepCopy(newCoord)
diff = {x = (newCoord.x - newGroupData.units[1].x), y = (newCoord.y - newGroupData.units[1].y)}
valid = true
break
end
end
if valid == false then
log:error('Point supplied in variable table is not a valid coordinate. Valid coords: $1', validTerrain)
return false
end
end
if not newGroupData.country and mist.DBs.groupsByName[newGroupData.groupName].country then
newGroupData.country = mist.DBs.groupsByName[newGroupData.groupName].country
end
if not newGroupData.category and mist.DBs.groupsByName[newGroupData.groupName].category then
newGroupData.category = mist.DBs.groupsByName[newGroupData.groupName].category
end
--log:info(point)
for unitNum, unitData in pairs(newGroupData.units) do
--log:info(unitNum)
if disperse then
local unitCoord
if maxDisp and type(maxDisp) == 'number' and unitNum ~= 1 then
for i = 1, 100 do
unitCoord = mist.getRandPointInCircle(origCoord, maxDisp)
if mist.isTerrainValid(unitCoord, validTerrain) == true then
--log:warn('Index: $1, Itered: $2. AT: $3', unitNum, i, unitCoord)
break
end
end
--else
--newCoord = mist.getRandPointInCircle(zone.point, zone.radius)
end
if unitNum == 1 then
unitCoord = mist.utils.deepCopy(newCoord)
end
if unitCoord then
newGroupData.units[unitNum].x = unitCoord.x
newGroupData.units[unitNum].y = unitCoord.y
end
else
newGroupData.units[unitNum].x = unitData.x + diff.x
newGroupData.units[unitNum].y = unitData.y + diff.y
end
if point then
if (newGroupData.category == 'plane' or newGroupData.category == 'helicopter') then
if point.z and point.y > 0 and point.y > land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + 10 then
newGroupData.units[unitNum].alt = point.y
--log:info('far enough from ground')
else
if newGroupData.category == 'plane' then
--log:info('setNewAlt')
newGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(300, 9000)
else
newGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(200, 3000)
end
end
end
end
end
if newGroupData.start_time then
newGroupData.startTime = newGroupData.start_time
end
if newGroupData.startTime and newGroupData.startTime ~= 0 and dbData == true then
local timeDif = timer.getAbsTime() - timer.getTime0()
if timeDif > newGroupData.startTime then
newGroupData.startTime = 0
else
newGroupData.startTime = newGroupData.startTime - timeDif
end
end
local tempRoute
if mist.DBs.MEgroupsByName[gpName] and not vars.route then
-- log:warn('getRoute')
tempRoute = mist.getGroupRoute(gpName, true)
elseif vars.route then
-- log:warn('routeExist')
tempRoute = mist.utils.deepCopy(vars.route)
end
-- log:warn(tempRoute)
if tempRoute then
if (vars.offsetRoute or vars.offsetWP1 or vars.initTasks) then
for i = 1, #tempRoute do
-- log:warn(i)
if (vars.offsetRoute) or (i == 1 and vars.offsetWP1) or (i == 1 and vars.initTasks) then
-- log:warn('update offset')
tempRoute[i].x = tempRoute[i].x + diff.x
tempRoute[i].y = tempRoute[i].y + diff.y
elseif vars.initTasks and i > 1 then
--log:warn('deleteWP')
tempRoute[i] = nil
end
end
end
newGroupData.route = tempRoute
end
--log:warn(newGroupData)
--mist.debug.writeData(mist.utils.serialize,{'teleportToPoint', newGroupData}, 'newGroupData.lua')
if string.lower(newGroupData.category) == 'static' then
--log:warn(newGroupData)
return mist.dynAddStatic(newGroupData)
end
return mist.dynAdd(newGroupData)
end
function mist.respawnInZone(gpName, zone, disperse, maxDisp, v)
if type(gpName) == 'table' and gpName:getName() then
gpName = gpName:getName()
elseif type(gpName) == 'table' and gpName[1]:getName() then
gpName = math.random(#gpName)
else
gpName = tostring(gpName)
end
if type(zone) == 'string' then
zone = mist.DBs.zonesByName[zone]
elseif type(zone) == 'table' and not zone.radius then
zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]]
end
local vars = {}
vars.gpName = gpName
vars.action = 'respawn'
vars.point = zone.point
vars.radius = zone.radius
vars.disperse = disperse
vars.maxDisp = maxDisp
if v and type(v) == 'table' then
for index, val in pairs(v) do
vars[index] = val
end
end
return mist.teleportToPoint(vars)
end
function mist.cloneInZone(gpName, zone, disperse, maxDisp, v)
--log:info('cloneInZone')
if type(gpName) == 'table' then
gpName = gpName:getName()
else
gpName = tostring(gpName)
end
if type(zone) == 'string' then
zone = mist.DBs.zonesByName[zone]
elseif type(zone) == 'table' and not zone.radius then
zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]]
end
local vars = {}
vars.gpName = gpName
vars.action = 'clone'
vars.point = zone.point
vars.radius = zone.radius
vars.disperse = disperse
vars.maxDisp = maxDisp
--log:info('do teleport')
if v and type(v) == 'table' then
for index, val in pairs(v) do
vars[index] = val
end
end
return mist.teleportToPoint(vars)
end
function mist.teleportInZone(gpName, zone, disperse, maxDisp, v) -- groupName, zoneName or table of Zone Names, keepForm is a boolean
if type(gpName) == 'table' and gpName:getName() then
gpName = gpName:getName()
else
gpName = tostring(gpName)
end
if type(zone) == 'string' then
zone = mist.DBs.zonesByName[zone]
elseif type(zone) == 'table' and not zone.radius then
zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]]
end
local vars = {}
vars.gpName = gpName
vars.action = 'tele'
vars.point = zone.point
vars.radius = zone.radius
vars.disperse = disperse
vars.maxDisp = maxDisp
if v and type(v) == 'table' then
for index, val in pairs(v) do
vars[index] = val
end
end
return mist.teleportToPoint(vars)
end
function mist.respawnGroup(gpName, task)
local vars = {}
vars.gpName = gpName
vars.action = 'respawn'
if task and type(task) ~= 'number' then
vars.route = mist.getGroupRoute(gpName, 'task')
end
local newGroup = mist.teleportToPoint(vars)
if task and type(task) == 'number' then
local newRoute = mist.getGroupRoute(gpName, 'task')
mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task)
end
return newGroup
end
function mist.cloneGroup(gpName, task)
local vars = {}
vars.gpName = gpName
vars.action = 'clone'
if task and type(task) ~= 'number' then
vars.route = mist.getGroupRoute(gpName, 'task')
end
local newGroup = mist.teleportToPoint(vars)
if task and type(task) == 'number' then
local newRoute = mist.getGroupRoute(gpName, 'task')
mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task)
end
return newGroup
end
function mist.teleportGroup(gpName, task)
local vars = {}
vars.gpName = gpName
vars.action = 'teleport'
if task and type(task) ~= 'number' then
vars.route = mist.getGroupRoute(gpName, 'task')
end
local newGroup = mist.teleportToPoint(vars)
if task and type(task) == 'number' then
local newRoute = mist.getGroupRoute(gpName, 'task')
mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task)
end
return newGroup
end
function mist.spawnRandomizedGroup(groupName, vars) -- need to debug
if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then
local gpData = mist.getGroupData(groupName)
gpData.units = mist.randomizeGroupOrder(gpData.units, vars)
gpData.route = mist.getGroupRoute(groupName, 'task')
mist.dynAdd(gpData)
end
return true
end
function mist.randomizeNumTable(vars)
local newTable = {}
local excludeIndex = {}
local randomTable = {}
if vars and vars.exclude and type(vars.exclude) == 'table' then
for index, data in pairs(vars.exclude) do
excludeIndex[data] = true
end
end
local low, hi, size
if vars.size then
size = vars.size
end
if vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then
low = mist.utils.round(vars.lowerLimit)
else
low = 1
end
if vars and vars.upperLimit and type(vars.upperLimit) == 'number' then
hi = mist.utils.round(vars.upperLimit)
else
hi = size
end
local choices = {}
-- add to exclude list and create list of what to randomize
for i = 1, size do
if not (i >= low and i <= hi) then
excludeIndex[i] = true
end
if not excludeIndex[i] then
table.insert(choices, i)
else
newTable[i] = i
end
end
for ind, num in pairs(choices) do
local found = false
local x = 0
while found == false do
x = mist.random(size) -- get random number from list
local addNew = true
for index, _ in pairs(excludeIndex) do
if index == x then
addNew = false
break
end
end
if addNew == true then
excludeIndex[x] = true
found = true
end
excludeIndex[x] = true
end
newTable[num] = x
end
--[[
for i = 1, #newTable do
log:info(newTable[i])
end
]]
return newTable
end
function mist.randomizeGroupOrder(passedUnits, vars)
-- figure out what to exclude, and send data to other func
local units = passedUnits
if passedUnits.units then
units = passUnits.units
end
local exclude = {}
local excludeNum = {}
if vars and vars.excludeType and type(vars.excludeType) == 'table' then
exclude = vars.excludeType
end
if vars and vars.excludeNum and type(vars.excludeNum) == 'table' then
excludeNum = vars.excludeNum
end
local low, hi
if vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then
low = mist.utils.round(vars.lowerLimit)
else
low = 1
end
if vars and vars.upperLimit and type(vars.upperLimit) == 'number' then
hi = mist.utils.round(vars.upperLimit)
else
hi = #units
end
local excludeNum = {}
for unitIndex, unitData in pairs(units) do
if unitIndex >= low and unitIndex <= hi then -- if within range
local found = false
if #exclude > 0 then
for excludeType, index in pairs(exclude) do -- check if excluded
if mist.stringMatch(excludeType, unitData.type) then -- if excluded
excludeNum[unitIndex] = unitIndex
found = true
end
end
end
else -- unitIndex is either to low, or to high: added to exclude list
excludeNum[unitIndex] = unitId
end
end
local newGroup = {}
local newOrder = mist.randomizeNumTable({exclude = excludeNum, size = #units})
for unitIndex, unitData in pairs(units) do
for i = 1, #newOrder do
if newOrder[i] == unitIndex then
newGroup[i] = mist.utils.deepCopy(units[i]) -- gets all of the unit data
newGroup[i].type = mist.utils.deepCopy(unitData.type)
newGroup[i].skill = mist.utils.deepCopy(unitData.skill)
newGroup[i].unitName = mist.utils.deepCopy(unitData.unitName)
newGroup[i].unitIndex = mist.utils.deepCopy(unitData.unitIndex) -- replaces the units data with a new type
end
end
end
return newGroup
end
function mist.random(firstNum, secondNum) -- no support for decimals
local lowNum, highNum
if not secondNum then
highNum = firstNum
lowNum = 1
else
lowNum = firstNum
highNum = secondNum
end
local total = 1
if math.abs(highNum - lowNum + 1) < 50 then -- if total values is less than 50
total = math.modf(50/math.abs(highNum - lowNum + 1)) -- make x copies required to be above 50
end
local choices = {}
for i = 1, total do -- iterate required number of times
for x = lowNum, highNum do -- iterate between the range
choices[#choices +1] = x -- add each entry to a table
end
end
local rtnVal = math.random(#choices) -- will now do a math.random of at least 50 choices
for i = 1, 10 do
rtnVal = math.random(#choices) -- iterate a few times for giggles
end
return choices[rtnVal]
end
function mist.stringCondense(s)
local exclude = {'%-', '%(', '%)', '%_', '%[', '%]', '%.', '%#', '% ', '%{', '%}', '%$', '%%', '%?', '%+', '%^'}
for i , str in pairs(exclude) do
s = string.gsub(s, str, '')
end
return s
end
function mist.stringMatch(s1, s2, bool)
if type(s1) == 'string' and type(s2) == 'string' then
s1 = mist.stringCondense(s1)
s2 = mist.stringCondense(s2)
if not bool then
s1 = string.lower(s1)
s2 = string.lower(s2)
end
--log:info('Comparing: $1 and $2', s1, s2)
if s1 == s2 then
return true
else
return false
end
else
log:error('Either the first or second variable were not a string')
return false
end
end
mist.matchString = mist.stringMatch -- both commands work because order out type of I
--[[ scope:
{
units = {...}, -- unit names.
coa = {...}, -- coa names
countries = {...}, -- country names
CA = {...}, -- looks just like coa.
unitTypes = { red = {}, blue = {}, all = {}, Russia = {},}
}
scope examples:
{ units = { 'Hawg11', 'Hawg12' }, CA = {'blue'} }
{ countries = {'Georgia'}, unitTypes = {blue = {'A-10C', 'A-10A'}}}
{ coa = {'all'}}
{unitTypes = { blue = {'A-10C'}}}
]]
end
--- Utility functions.
-- E.g. conversions between units etc.
-- @section mist.utils
do -- mist.util scope
mist.utils = {}
--- Converts angle in radians to degrees.
-- @param angle angle in radians
-- @return angle in degrees
function mist.utils.toDegree(angle)
return angle*180/math.pi
end
--- Converts angle in degrees to radians.
-- @param angle angle in degrees
-- @return angle in degrees
function mist.utils.toRadian(angle)
return angle*math.pi/180
end
--- Converts meters to nautical miles.
-- @param meters distance in meters
-- @return distance in nautical miles
function mist.utils.metersToNM(meters)
return meters/1852
end
--- Converts meters to feet.
-- @param meters distance in meters
-- @return distance in feet
function mist.utils.metersToFeet(meters)
return meters/0.3048
end
--- Converts nautical miles to meters.
-- @param nm distance in nautical miles
-- @return distance in meters
function mist.utils.NMToMeters(nm)
return nm*1852
end
--- Converts feet to meters.
-- @param feet distance in feet
-- @return distance in meters
function mist.utils.feetToMeters(feet)
return feet*0.3048
end
--- Converts meters per second to knots.
-- @param mps speed in m/s
-- @return speed in knots
function mist.utils.mpsToKnots(mps)
return mps*3600/1852
end
--- Converts meters per second to kilometers per hour.
-- @param mps speed in m/s
-- @return speed in km/h
function mist.utils.mpsToKmph(mps)
return mps*3.6
end
--- Converts knots to meters per second.
-- @param knots speed in knots
-- @return speed in m/s
function mist.utils.knotsToMps(knots)
return knots*1852/3600
end
--- Converts kilometers per hour to meters per second.
-- @param kmph speed in km/h
-- @return speed in m/s
function mist.utils.kmphToMps(kmph)
return kmph/3.6
end
function mist.utils.kelvinToCelsius(t)
return t - 273.15
end
function mist.utils.FahrenheitToCelsius(f)
return (f - 32) * (5/9)
end
function mist.utils.celsiusToFahrenheit(c)
return c*(9/5)+32
end
function mist.utils.hexToRGB(hex, l) -- because for some reason the draw tools use hex when everything is rgba 0 - 1
local int = 255
if l then
int = 1
end
if hex and type(hex) == 'string' then
local val = {}
hex = string.gsub(hex, '0x', '')
if string.len(hex) == 8 then
val[1] = tonumber("0x"..hex:sub(1,2)) / int
val[2] = tonumber("0x"..hex:sub(3,4)) / int
val[3] = tonumber("0x"..hex:sub(5,6)) / int
val[4] = tonumber("0x"..hex:sub(7,8)) / int
return val
end
end
end
function mist.utils.converter(t1, t2, val)
if type(t1) == 'string' then
t1 = string.lower(t1)
end
if type(t2) == 'string' then
t2 = string.lower(t2)
end
if val and type(val) ~= 'number' then
if tonumber(val) then
val = tonumber(val)
else
log:warn("Value given is not a number: $1", val)
return 0
end
end
-- speed
if t1 == 'mps' then
if t2 == 'kmph' then
return val * 3.6
elseif t2 == 'knots' or t2 == 'knot' then
return val * 3600/1852
end
elseif t1 == 'kmph' then
if t2 == 'mps' then
return val/3.6
elseif t2 == 'knots' or t2 == 'knot' then
return val*0.539957
end
elseif t1 == 'knot' or t1 == 'knots' then
if t2 == 'kmph' then
return val * 1.852
elseif t2 == 'mps' then
return val * 0.514444
end
-- Distance
elseif t1 == 'feet' or t1 == 'ft' then
if t2 == 'nm' then
return val/6076.12
elseif t2 == 'km' then
return (val*0.3048)/1000
elseif t2 == 'm' then
return val*0.3048
end
elseif t1 == 'nm' then
if t2 == 'feet' or t2 == 'ft' then
return val*6076.12
elseif t2 == 'km' then
return val*1.852
elseif t2 == 'm' then
return val*1852
end
elseif t1 == 'km' then
if t2 == 'nm' then
return val/1.852
elseif t2 == 'feet' or t2 == 'ft' then
return (val/0.3048)*1000
elseif t2 == 'm' then
return val*1000
end
elseif t1 == 'm' then
if t2 == 'nm' then
return val/1852
elseif t2 == 'km' then
return val/1000
elseif t2 == 'feet' or t2 == 'ft' then
return val/0.3048
end
-- Temperature
elseif t1 == 'f' or t1 == 'fahrenheit' then
if t2 == 'c' or t2 == 'celsius' then
return (val - 32) * (5/9)
elseif t2 == 'k' or t2 == 'kelvin' then
return (val + 459.67) * (5/9)
end
elseif t1 == 'c' or t1 == 'celsius' then
if t2 == 'f' or t2 == 'fahrenheit' then
return val*(9/5)+32
elseif t2 == 'k' or t2 == 'kelvin' then
return val + 273.15
end
elseif t1 == 'k' or t1 == 'kelvin' then
if t2 == 'c' or t2 == 'celsius' then
return val - 273.15
elseif t2 == 'f' or t2 == 'fahrenheit' then
return ((val*(9/5))-459.67)
end
-- Pressure
elseif t1 == 'p' or t1 == 'pascal' or t1 == 'pascals' then
if t2 == 'hpa' or t2 == 'hectopascal' then
return val/100
elseif t2 == 'mmhg' then
return val * 0.00750061561303
elseif t2 == 'inhg' then
return val * 0.0002953
end
elseif t1 == 'hpa' or t1 == 'hectopascal' then
if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then
return val*100
elseif t2 == 'mmhg' then
return val * 0.00750061561303
elseif t2 == 'inhg' then
return val * 0.02953
end
elseif t1 == 'mmhg' then
if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then
return val / 0.00750061561303
elseif t2 == 'hpa' or t2 == 'hectopascal' then
return val * 1.33322
elseif t2 == 'inhg' then
return val/25.4
end
elseif t1 == 'inhg' then
if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then
return val*3386.39
elseif t2 == 'mmhg' then
return val*25.4
elseif t2 == 'hpa' or t2 == 'hectopascal' then
return val * 33.8639
end
else
log:warn("First value doesn't match with list. Value given: $1", t1)
end
log:warn("Match not found. Unable to convert: $1 into $2", t1, t2)
end
mist.converter = mist.utils.converter
function mist.utils.getQFE(point, inchHg)
local t, p = 0, 0
if atmosphere.getTemperatureAndPressure then
t, p = atmosphere.getTemperatureAndPressure(mist.utils.makeVec3GL(point))
end
if p == 0 then
local h = land.getHeight(mist.utils.makeVec2(point))/0.3048 -- convert to feet
if inchHg then
return (env.mission.weather.qnh - (h/30)) * 0.0295299830714
else
return env.mission.weather.qnh - (h/30)
end
else
if inchHg then
return mist.converter('p', 'inhg', p)
else
return mist.converter('p', 'hpa', p)
end
end
end
--- Converts a Vec3 to a Vec2.
-- @tparam Vec3 vec the 3D vector
-- @return vector converted to Vec2
function mist.utils.makeVec2(vec)
if vec.z then
return {x = vec.x, y = vec.z}
else
return {x = vec.x, y = vec.y} -- it was actually already vec2.
end
end
--- Converts a Vec2 to a Vec3.
-- @tparam Vec2 vec the 2D vector
-- @param y optional new y axis (altitude) value. If omitted it's 0.
function mist.utils.makeVec3(vec, y)
if not vec.z then
if vec.alt and not y then
y = vec.alt
elseif not y then
y = 0
end
return {x = vec.x, y = y, z = vec.y}
else
return {x = vec.x, y = vec.y, z = vec.z} -- it was already Vec3, actually.
end
end
--- Converts a Vec2 to a Vec3 using ground level as altitude.
-- The ground level at the specific point is used as altitude (y-axis)
-- for the new vector. Optionally a offset can be specified.
-- @tparam Vec2 vec the 2D vector
-- @param[opt] offset offset to be applied to the ground level
-- @return new 3D vector
function mist.utils.makeVec3GL(vec, offset)
local adj = offset or 0
if not vec.z then
return {x = vec.x, y = (land.getHeight(vec) + adj), z = vec.y}
else
return {x = vec.x, y = (land.getHeight({x = vec.x, y = vec.z}) + adj), z = vec.z}
end
end
--- Returns the center of a zone as Vec3.
-- @tparam string|table zone trigger zone name or table
-- @treturn Vec3 center of the zone
function mist.utils.zoneToVec3(zone, gl)
local new = {}
if type(zone) == 'table' then
if zone.point then
new.x = zone.point.x
new.y = zone.point.y
new.z = zone.point.z
elseif zone.x and zone.y and zone.z then
new = mist.utils.deepCopy(zone)
end
return new
elseif type(zone) == 'string' then
zone = trigger.misc.getZone(zone)
if zone then
new.x = zone.point.x
new.y = zone.point.y
new.z = zone.point.z
end
end
if new.x and gl then
new.y = land.getHeight({x = new.x, y = new.z})
end
return new
end
function mist.utils.getHeadingPoints(point1, point2, north) -- sick of writing this out.
if north then
local p1 = mist.utils.get3DDist(point1)
return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), p1), p1)
else
return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1)))
end
end
--- Returns heading-error corrected direction.
-- True-north corrected direction from point along vector vec.
-- @tparam Vec3 vec
-- @tparam Vec2 point
-- @return heading-error corrected direction from point.
function mist.utils.getDir(vec, point)
local dir = math.atan2(vec.z, vec.x)
if point then
dir = dir + mist.getNorthCorrection(point)
end
if dir < 0 then
dir = dir + 2 * math.pi -- put dir in range of 0 to 2*pi
end
return dir
end
--- Returns distance in meters between two points.
-- @tparam Vec2|Vec3 point1 first point
-- @tparam Vec2|Vec3 point2 second point
-- @treturn number distance between given points.
function mist.utils.get2DDist(point1, point2)
if not point1 then
log:warn("mist.utils.get2DDist 1st input value is nil")
end
if not point2 then
log:warn("mist.utils.get2DDist 2nd input value is nil")
end
point1 = mist.utils.makeVec3(point1)
point2 = mist.utils.makeVec3(point2)
return mist.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z})
end
--- Returns distance in meters between two points in 3D space.
-- @tparam Vec3 point1 first point
-- @tparam Vec3 point2 second point
-- @treturn number distancen between given points in 3D space.
function mist.utils.get3DDist(point1, point2)
if not point1 then
log:warn("mist.utils.get2DDist 1st input value is nil")
end
if not point2 then
log:warn("mist.utils.get2DDist 2nd input value is nil")
end
return mist.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z})
end
--- Creates a waypoint from a vector.
-- @tparam Vec2|Vec3 vec position of the new waypoint
-- @treturn Waypoint a new waypoint to be used inside paths.
function mist.utils.vecToWP(vec)
local newWP = {}
newWP.x = vec.x
newWP.y = vec.y
if vec.z then
newWP.alt = vec.y
newWP.y = vec.z
else
newWP.alt = land.getHeight({x = vec.x, y = vec.y})
end
return newWP
end
--- Creates a waypoint from a unit.
-- This function also considers the units speed.
-- The alt_type of this waypoint is set to "BARO".
-- @tparam Unit pUnit Unit whose position and speed will be used.
-- @treturn Waypoint new waypoint.
function mist.utils.unitToWP(pUnit)
local unit = mist.utils.deepCopy(pUnit)
if type(unit) == 'string' then
if Unit.getByName(unit) then
unit = Unit.getByName(unit)
end
end
if unit:isExist() == true then
local new = mist.utils.vecToWP(unit:getPosition().p)
new.speed = mist.vec.mag(unit:getVelocity())
new.alt_type = "BARO"
return new
end
log:error("$1 not found or doesn't exist", pUnit)
return false
end
--- Creates a deep copy of a object.
-- Usually this object is a table.
-- See also: from http://lua-users.org/wiki/CopyTable
-- @param object object to copy
-- @return copy of object
function mist.utils.deepCopy(object)
local lookup_table = {}
local function _copy(object)
if type(object) ~= "table" then
return object
elseif lookup_table[object] then
return lookup_table[object]
end
local new_table = {}
lookup_table[object] = new_table
for index, value in pairs(object) do
new_table[_copy(index)] = _copy(value)
end
return setmetatable(new_table, getmetatable(object))
end
return _copy(object)
end
--- Simple rounding function.
-- From http://lua-users.org/wiki/SimpleRound
-- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place
-- @tparam number num number to round
-- @param idp
function mist.utils.round(num, idp)
local mult = 10^(idp or 0)
return math.floor(num * mult + 0.5) / mult
end
--- Rounds all numbers inside a table.
-- @tparam table tbl table in which to round numbers
-- @param idp
function mist.utils.roundTbl(tbl, idp)
for id, val in pairs(tbl) do
if type(val) == 'number' then
tbl[id] = mist.utils.round(val, idp)
end
end
return tbl
end
--- Executes the given string.
-- borrowed from Slmod
-- @tparam string s string containing LUA code.
-- @treturn boolean true if successfully executed, false otherwise
function mist.utils.dostring(s)
local f, err = loadstring(s)
if f then
return true, f()
else
return false, err
end
end
--- Checks a table's types.
-- This function checks a tables types against a specifically forged type table.
-- @param fname
-- @tparam table type_tbl
-- @tparam table var_tbl
-- @usage -- specifically forged type table
-- type_tbl = {
-- {'table', 'number'},
-- 'string',
-- 'number',
-- 'number',
-- {'string','nil'},
-- {'number', 'nil'}
-- }
-- -- my_tbl index 1 must be a table or a number;
-- -- index 2, a string; index 3, a number;
-- -- index 4, a number; index 5, either a string or nil;
-- -- and index 6, either a number or nil.
-- mist.utils.typeCheck(type_tbl, my_tb)
-- @return true if table passes the check, false otherwise.
function mist.utils.typeCheck(fname, type_tbl, var_tbl)
-- log:info('type check')
for type_key, type_val in pairs(type_tbl) do
-- log:info('type_key: $1 type_val: $2', type_key, type_val)
--type_key can be a table of accepted keys- so try to find one that is not nil
local type_key_str = ''
local act_key = type_key -- actual key within var_tbl - necessary to use for multiple possible key variables. Initialize to type_key
if type(type_key) == 'table' then
for i = 1, #type_key do
if i ~= 1 then
type_key_str = type_key_str .. '/'
end
type_key_str = type_key_str .. tostring(type_key[i])
if var_tbl[type_key[i]] ~= nil then
act_key = type_key[i] -- found a non-nil entry, make act_key now this val.
end
end
else
type_key_str = tostring(type_key)
end
local err_msg = 'Error in function ' .. fname .. ', parameter "' .. type_key_str .. '", expected: '
local passed_check = false
if type(type_tbl[type_key]) == 'table' then
-- log:info('err_msg, before: $1', err_msg)
for j = 1, #type_tbl[type_key] do
if j == 1 then
err_msg = err_msg .. type_tbl[type_key][j]
else
err_msg = err_msg .. ' or ' .. type_tbl[type_key][j]
end
if type(var_tbl[act_key]) == type_tbl[type_key][j] then
passed_check = true
end
end
-- log:info('err_msg, after: $1', err_msg)
else
-- log:info('err_msg, before: $1', err_msg)
err_msg = err_msg .. type_tbl[type_key]
-- log:info('err_msg, after: $1', err_msg)
if type(var_tbl[act_key]) == type_tbl[type_key] then
passed_check = true
end
end
if not passed_check then
err_msg = err_msg .. ', got ' .. type(var_tbl[act_key])
return false, err_msg
end
end
return true
end
--- Serializes the give variable to a string.
-- borrowed from slmod
-- @param var variable to serialize
-- @treturn string variable serialized to string
function mist.utils.basicSerialize(var)
if var == nil then
return "\"\""
else
if ((type(var) == 'number') or
(type(var) == 'boolean') or
(type(var) == 'function') or
(type(var) == 'table') or
(type(var) == 'userdata') ) then
return tostring(var)
elseif type(var) == 'string' then
var = string.format('%q', var)
return var
end
end
end
--- Serialize value
-- borrowed from slmod (serialize_slmod)
-- @param name
-- @param value value to serialize
-- @param level
function mist.utils.serialize(name, value, level)
--Based on ED's serialize_simple2
local function basicSerialize(o)
if type(o) == "number" then
return tostring(o)
elseif type(o) == "boolean" then
return tostring(o)
else -- assume it is a string
return mist.utils.basicSerialize(o)
end
end
local function serializeToTbl(name, value, level)
local var_str_tbl = {}
if level == nil then
level = ""
end
if level ~= "" then
level = level..""
end
table.insert(var_str_tbl, level .. name .. " = ")
if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then
table.insert(var_str_tbl, basicSerialize(value) .. ",\n")
elseif type(value) == "table" then
table.insert(var_str_tbl, "\n"..level.."{\n")
for k,v in pairs(value) do -- serialize its fields
local key
if type(k) == "number" then
key = string.format("[%s]", k)
else
key = string.format("[%q]", k)
end
table.insert(var_str_tbl, mist.utils.serialize(key, v, level.." "))
end
if level == "" then
table.insert(var_str_tbl, level.."} -- end of "..name.."\n")
else
table.insert(var_str_tbl, level.."}, -- end of "..name.."\n")
end
else
log:error('Cannot serialize a $1', type(value))
end
return var_str_tbl
end
local t_str = serializeToTbl(name, value, level)
return table.concat(t_str)
end
--- Serialize value supporting cycles.
-- borrowed from slmod (serialize_wcycles)
-- @param name
-- @param value value to serialize
-- @param saved
function mist.utils.serializeWithCycles(name, value, saved)
--mostly straight out of Programming in Lua
local function basicSerialize(o)
if type(o) == "number" then
return tostring(o)
elseif type(o) == "boolean" then
return tostring(o)
else -- assume it is a string
return mist.utils.basicSerialize(o)
end
end
local t_str = {}
saved = saved or {} -- initial value
if ((type(value) == 'string') or (type(value) == 'number') or (type(value) == 'table') or (type(value) == 'boolean')) then
table.insert(t_str, name .. " = ")
if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then
table.insert(t_str, basicSerialize(value) .. "\n")
else
if saved[value] then -- value already saved?
table.insert(t_str, saved[value] .. "\n")
else
saved[value] = name -- save name for next time
table.insert(t_str, "{}\n")
for k,v in pairs(value) do -- save its fields
local fieldname = string.format("%s[%s]", name, basicSerialize(k))
table.insert(t_str, mist.utils.serializeWithCycles(fieldname, v, saved))
end
end
end
return table.concat(t_str)
else
return ""
end
end
--- Serialize a table to a single line string.
-- serialization of a table all on a single line, no comments, made to replace old get_table_string function
-- borrowed from slmod
-- @tparam table tbl table to serialize.
-- @treturn string string containing serialized table
function mist.utils.oneLineSerialize(tbl)
if type(tbl) == 'table' then --function only works for tables!
local tbl_str = {}
tbl_str[#tbl_str + 1] = '{ '
for ind,val in pairs(tbl) do -- serialize its fields
if type(ind) == "number" then
tbl_str[#tbl_str + 1] = '['
tbl_str[#tbl_str + 1] = tostring(ind)
tbl_str[#tbl_str + 1] = '] = '
else --must be a string
tbl_str[#tbl_str + 1] = '['
tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind)
tbl_str[#tbl_str + 1] = '] = '
end
if ((type(val) == 'number') or (type(val) == 'boolean')) then
tbl_str[#tbl_str + 1] = tostring(val)
tbl_str[#tbl_str + 1] = ', '
elseif type(val) == 'string' then
tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val)
tbl_str[#tbl_str + 1] = ', '
elseif type(val) == 'nil' then -- won't ever happen, right?
tbl_str[#tbl_str + 1] = 'nil, '
elseif type(val) == 'table' then
tbl_str[#tbl_str + 1] = mist.utils.oneLineSerialize(val)
tbl_str[#tbl_str + 1] = ', ' --I think this is right, I just added it
else
log:warn('Unable to serialize value type $1 at index $2', mist.utils.basicSerialize(type(val)), tostring(ind))
end
end
tbl_str[#tbl_str + 1] = '}'
return table.concat(tbl_str)
else
return mist.utils.basicSerialize(tbl)
end
end
function mist.utils.tableShowSorted(tbls, v)
local vars = v or {}
local loc = vars.loc or ""
local indent = vars.indent or ""
local tableshow_tbls = vars.tableshow_tbls or {}
local tbl = tbls or {}
if type(tbl) == 'table' then --function only works for tables!
tableshow_tbls[tbl] = loc
local tbl_str = {}
tbl_str[#tbl_str + 1] = indent .. '{\n'
local sorted = {}
local function byteCompare(str1, str2)
local shorter = string.len(str1)
if shorter > string.len(str2) then
shorter = string.len(str2)
end
for i = 1, shorter do
local b1 = string.byte(str1, i)
local b2 = string.byte(str2, i)
if b1 < b2 then
return true
elseif b1 > b2 then
return false
end
end
return false
end
for ind, val in pairs(tbl) do -- serialize its fields
local indS = tostring(ind)
local ins = {ind = indS, val = val}
local index
if #sorted > 0 then
local found = false
for i = 1, #sorted do
if byteCompare(indS, tostring(sorted[i].ind)) == true then
index = i
break
end
end
end
if index then
table.insert(sorted, index, ins)
else
table.insert(sorted, ins)
end
end
--log:warn(sorted)
for i = 1, #sorted do
local ind = sorted[i].ind
local val = sorted[i].val
if type(ind) == "number" then
tbl_str[#tbl_str + 1] = indent
tbl_str[#tbl_str + 1] = loc .. '['
tbl_str[#tbl_str + 1] = tostring(ind)
tbl_str[#tbl_str + 1] = '] = '
else
tbl_str[#tbl_str + 1] = indent
tbl_str[#tbl_str + 1] = loc .. '['
tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind)
tbl_str[#tbl_str + 1] = '] = '
end
if ((type(val) == 'number') or (type(val) == 'boolean')) then
tbl_str[#tbl_str + 1] = tostring(val)
tbl_str[#tbl_str + 1] = ',\n'
elseif type(val) == 'string' then
tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val)
tbl_str[#tbl_str + 1] = ',\n'
elseif type(val) == 'nil' then -- won't ever happen, right?
tbl_str[#tbl_str + 1] = 'nil,\n'
elseif type(val) == 'table' then
if tableshow_tbls[val] then
tbl_str[#tbl_str + 1] = ' already defined: ' .. tableshow_tbls[val] .. ',\n'
else
tableshow_tbls[val] = loc .. '["' .. ind .. '"]'
--tbl_str[#tbl_str + 1] = tostring(val) .. ' '
tbl_str[#tbl_str + 1] = mist.utils.tableShowSorted(val, {loc = loc .. '["' .. ind .. '"]', indent = indent .. ' ', tableshow_tbls = tableshow_tbls})
tbl_str[#tbl_str + 1] = ',\n'
end
elseif type(val) == 'function' then
if debug and debug.getinfo then
local fcnname = tostring(val)
local info = debug.getinfo(val, "S")
if info.what == "C" then
tbl_str[#tbl_str + 1] = ', C function\n'
else
if (string.sub(info.source, 1, 2) == [[./]]) then
tbl_str[#tbl_str + 1] = string.format('%q', 'function, defined in (' .. '-' .. info.lastlinedefined .. ')' .. info.source) ..',\n'
else
tbl_str[#tbl_str + 1] = string.format('%q', 'function, defined in (' .. '-' .. info.lastlinedefined .. ')') ..',\n'
end
end
else
tbl_str[#tbl_str + 1] = 'a function,\n'
end
else
tbl_str[#tbl_str + 1] = 'unable to serialize value type ' .. mist.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)
end
end
tbl_str[#tbl_str + 1] = indent .. '}'
return table.concat(tbl_str)
end
end
--- Returns table in a easy readable string representation.
-- this function is not meant for serialization because it uses
-- newlines for better readability.
-- @param tbl table to show
-- @param loc
-- @param indent
-- @param tableshow_tbls
-- @return human readable string representation of given table
function mist.utils.tableShow(tbl, loc, indent, tableshow_tbls) --based on serialize_slmod, this is a _G serialization
tableshow_tbls = tableshow_tbls or {} --create table of tables
loc = loc or ""
indent = indent or ""
if type(tbl) == 'table' then --function only works for tables!
tableshow_tbls[tbl] = loc
local tbl_str = {}
tbl_str[#tbl_str + 1] = indent .. '{\n'
for ind, val in pairs(tbl) do
if type(ind) == "number" then
tbl_str[#tbl_str + 1] = indent
tbl_str[#tbl_str + 1] = loc .. '['
tbl_str[#tbl_str + 1] = tostring(ind)
tbl_str[#tbl_str + 1] = '] = '
else
tbl_str[#tbl_str + 1] = indent
tbl_str[#tbl_str + 1] = loc .. '['
tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind)
tbl_str[#tbl_str + 1] = '] = '
end
if ((type(val) == 'number') or (type(val) == 'boolean')) then
tbl_str[#tbl_str + 1] = tostring(val)
tbl_str[#tbl_str + 1] = ',\n'
elseif type(val) == 'string' then
tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val)
tbl_str[#tbl_str + 1] = ',\n'
elseif type(val) == 'nil' then -- won't ever happen, right?
tbl_str[#tbl_str + 1] = 'nil,\n'
elseif type(val) == 'table' then
if tableshow_tbls[val] then
tbl_str[#tbl_str + 1] = tostring(val) .. ' already defined: ' .. tableshow_tbls[val] .. ',\n'
else
tableshow_tbls[val] = loc .. '[' .. mist.utils.basicSerialize(ind) .. ']'
tbl_str[#tbl_str + 1] = tostring(val) .. ' '
tbl_str[#tbl_str + 1] = mist.utils.tableShow(val, loc .. '[' .. mist.utils.basicSerialize(ind).. ']', indent .. ' ', tableshow_tbls)
tbl_str[#tbl_str + 1] = ',\n'
end
elseif type(val) == 'function' then
if debug and debug.getinfo then
local fcnname = tostring(val)
local info = debug.getinfo(val, "S")
if info.what == "C" then
tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', C function') .. ',\n'
else
if (string.sub(info.source, 1, 2) == [[./]]) then
tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')' .. info.source) ..',\n'
else
tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')') ..',\n'
end
end
else
tbl_str[#tbl_str + 1] = 'a function,\n'
end
else
tbl_str[#tbl_str + 1] = 'unable to serialize value type ' .. mist.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)
end
end
tbl_str[#tbl_str + 1] = indent .. '}'
return table.concat(tbl_str)
end
end
end
--- Debug functions
-- @section mist.debug
do -- mist.debug scope
mist.debug = {}
function mist.debug.changeSetting(s)
if type(s) == 'table' then
for sName, sVal in pairs(s) do
if type(sVal) == 'string' or type(sVal) == 'number' then
if sName == 'log' then
mistSettings[sName] = sVal
mist.log:setLevel(sVal)
elseif sName == 'dbLog' then
mistSettings[sName] = sVal
dblog:setLevel(sVal)
end
else
mistSettings[sName] = sVal
end
end
end
end
--- Dumps the global table _G.
-- This dumps the global table _G to a file in
-- the DCS\Logs directory.
-- This function requires you to disable script sanitization
-- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io
-- libraries.
-- @param fname
function mist.debug.dump_G(fname, simp)
if lfs and io then
local fdir = lfs.writedir() .. [[Logs\]] .. fname
local f = io.open(fdir, 'w')
if simp then
local g = mist.utils.deepCopy(_G)
g.mist = nil
g.slmod = nil
g.env.mission = nil
g.env.warehouses = nil
g.country.by_idx = nil
g.country.by_country = nil
f:write(mist.utils.tableShowSorted(g))
else
f:write(mist.utils.tableShowSorted(_G))
end
f:close()
log:info('Wrote debug data to $1', fdir)
--trigger.action.outText(errmsg, 10)
else
log:alert('insufficient libraries to run mist.debug.dump_G, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua')
--trigger.action.outText(errmsg, 10)
end
end
--- Write debug data to file.
-- This function requires you to disable script sanitization
-- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io
-- libraries.
-- @param fcn
-- @param fcnVars
-- @param fname
function mist.debug.writeData(fcn, fcnVars, fname)
if lfs and io then
local fdir = lfs.writedir() .. [[Logs\]] .. fname
local f = io.open(fdir, 'w')
f:write(fcn(unpack(fcnVars, 1, table.maxn(fcnVars))))
f:close()
log:info('Wrote debug data to $1', fdir)
local errmsg = 'mist.debug.writeData wrote data to ' .. fdir
trigger.action.outText(errmsg, 10)
else
local errmsg = 'Error: insufficient libraries to run mist.debug.writeData, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua'
log:alert(errmsg)
trigger.action.outText(errmsg, 10)
end
end
--- Write mist databases to file.
-- This function requires you to disable script sanitization
-- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io
-- libraries.
function mist.debug.dumpDBs()
for DBname, DB in pairs(mist.DBs) do
if type(DB) == 'table' and type(DBname) == 'string' then
mist.debug.writeData(mist.utils.serialize, {DBname, DB}, 'mist_DBs_' .. DBname .. '.lua')
end
end
end
-- write group table
function mist.debug.writeGroup(gName, data)
if gName and mist.DBs.groupsByName[gName] then
local dat
if data then
dat = mist.getGroupData(gName)
else
dat = mist.getGroupTable(gName)
end
if dat then
dat.route = {points = mist.getGroupRoute(gName, true)}
end
if io and lfs and dat then
mist.debug.writeData(mist.utils.serialize, {gName, dat}, gName .. '_table.lua')
else
if dat then
trigger.action.outText('Error: insufficient libraries to run mist.debug.writeGroup, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua \nGroup table written to DCS.log file instead.', 10)
log:warn('$1 dataTable: $2', gName, dat)
else
trigger.action.outText('Unable to write group table for: ' .. gName .. '\n Error: insufficient libraries to run mist.debug.writeGroup, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua', 10)
end
end
end
end
-- write all object types in mission.
function mist.debug.writeTypes(fName)
local wt = 'mistDebugWriteTypes.lua'
if fName and type(fName) == 'string' and string.find(fName, '.lua') then
wt = fName
end
local output = {units = {}, countries = {}}
for coa_name_miz, coa_data in pairs(env.mission.coalition) do
if type(coa_data) == 'table' then
if coa_data.country then --there is a country table
for cntry_id, cntry_data in pairs(coa_data.country) do
local countryName = string.lower(cntry_data.name)
if cntry_data.id and country.names[cntry_data.id] then
countryName = string.lower(country.names[cntry_data.id])
end
output.countries[countryName] = {}
if type(cntry_data) == 'table' then --just making sure
for obj_cat_name, obj_cat_data in pairs(cntry_data) do
if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then --should be an unncessary check
local category = obj_cat_name
if not output.countries[countryName][category] then
-- log:warn('Create: $1', category)
output.countries[countryName][category] = {}
end
if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group!
for group_num, group_data in pairs(obj_cat_data.group) do
if group_data and group_data.units and type(group_data.units) == 'table' then --making sure again- this is a valid group
for i = 1, #group_data.units do
if group_data.units[i] then
local u = group_data.units[i]
local liv = u.livery_id or 'default'
if not output.units[u.type] then -- create unit table
-- log:warn('Create: $1', u.type)
output.units[u.type] = {count = 0, livery_id = {}}
end
if not output.countries[countryName][category][u.type] then
-- log:warn('Create country, category, unit: $1', u.type)
output.countries[countryName][category][u.type] = 0
end
-- add to count
output.countries[countryName][category][u.type] = output.countries[countryName][category][u.type] + 1
output.units[u.type].count = output.units[u.type].count + 1
if liv and not output.units[u.type].livery_id[countryName] then
-- log:warn('Create livery country: $1', countryName)
output.units[u.type].livery_id[countryName] = {}
end
if liv and not output.units[u.type].livery_id[countryName][liv] then
--log:warn('Create Livery: $1', liv)
output.units[u.type].livery_id[countryName][liv] = 0
end
if liv then
output.units[u.type].livery_id[countryName][liv] = output.units[u.type].livery_id[countryName][liv] + 1
end
if u.payload and u.payload.pylons then
if not output.units[u.type].CLSID then
output.units[u.type].CLSID = {}
output.units[u.type].pylons = {}
end
for pyIndex, pData in pairs(u.payload.pylons) do
if not output.units[u.type].CLSID[pData.CLSID] then
output.units[u.type].CLSID[pData.CLSID] = 0
end
output.units[u.type].CLSID[pData.CLSID] = output.units[u.type].CLSID[pData.CLSID] + 1
if not output.units[u.type].pylons[pyIndex] then
output.units[u.type].pylons[pyIndex] = {}
end
if not output.units[u.type].pylons[pyIndex][pData.CLSID] then
output.units[u.type].pylons[pyIndex][pData.CLSID] = 0
end
output.units[u.type].pylons[pyIndex][pData.CLSID] = output.units[u.type].pylons[pyIndex][pData.CLSID] + 1
end
end
end
end
end
end
end
end
end
end
end
end
end
end
if io and lfs then
mist.debug.writeData(mist.utils.serialize, {'mistDebugWriteTypes', output}, wt)
else
trigger.action.outText('Error: insufficient libraries to run mist.debug.writeTypes, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua \n writeTypes table written to DCS.log file instead.', 10)
log:warn('mist.debug.writeTypes: $1', output)
end
return output
end
function mist.debug.writeWeapons(unit)
end
function mist.debug.mark(msg, coord)
mist.marker.add({point = coord, text = msg})
log:warn('debug.mark: $1 $2', msg, coord)
end
end
--- 3D Vector functions
-- @section mist.vec
do -- mist.vec scope
mist.vec = {}
--- Vector addition.
-- @tparam Vec3 vec1 first vector
-- @tparam Vec3 vec2 second vector
-- @treturn Vec3 new vector, sum of vec1 and vec2.
function mist.vec.add(vec1, vec2)
return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z}
end
--- Vector substraction.
-- @tparam Vec3 vec1 first vector
-- @tparam Vec3 vec2 second vector
-- @treturn Vec3 new vector, vec2 substracted from vec1.
function mist.vec.sub(vec1, vec2)
return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z}
end
--- Vector scalar multiplication.
-- @tparam Vec3 vec vector to multiply
-- @tparam number mult scalar multiplicator
-- @treturn Vec3 new vector multiplied with the given scalar
function mist.vec.scalarMult(vec, mult)
return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult}
end
mist.vec.scalar_mult = mist.vec.scalarMult
--- Vector dot product.
-- @tparam Vec3 vec1 first vector
-- @tparam Vec3 vec2 second vector
-- @treturn number dot product of given vectors
function mist.vec.dp (vec1, vec2)
return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z
end
--- Vector cross product.
-- @tparam Vec3 vec1 first vector
-- @tparam Vec3 vec2 second vector
-- @treturn Vec3 new vector, cross product of vec1 and vec2.
function mist.vec.cp(vec1, vec2)
return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x}
end
--- Vector magnitude
-- @tparam Vec3 vec vector
-- @treturn number magnitude of vector vec
function mist.vec.mag(vec)
return (vec.x^2 + vec.y^2 + vec.z^2)^0.5
end
--- Unit vector
-- @tparam Vec3 vec
-- @treturn Vec3 unit vector of vec
function mist.vec.getUnitVec(vec)
local mag = mist.vec.mag(vec)
return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag }
end
--- Rotate vector.
-- @tparam Vec2 vec2 to rotoate
-- @tparam number theta
-- @return Vec2 rotated vector.
function mist.vec.rotateVec2(vec2, theta)
return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)}
end
function mist.vec.normalize(vec3)
local mag = mist.vec.mag(vec3)
if mag ~= 0 then
return mist.vec.scalar_mult(vec3, 1.0 / mag)
end
end
end
--- Flag functions.
-- The mist "Flag functions" are functions that are similar to Slmod functions
-- that detect a game condition and set a flag when that game condition is met.
--
-- They are intended to be used by persons with little or no experience in Lua
-- programming, but with a good knowledge of the DCS mission editor.
-- @section mist.flagFunc
do -- mist.flagFunc scope
mist.flagFunc = {}
--- Sets a flag if map objects are destroyed inside a zone.
-- Once this function is run, it will start a continuously evaluated process
-- that will set a flag true if map objects (such as bridges, buildings in
-- town, etc.) die (or have died) in a mission editor zone (or set of zones).
-- This will only happen once; once the flag is set true, the process ends.
-- @usage
-- -- Example vars table
-- vars = {
-- zones = { "zone1", "zone2" }, -- can also be a single string
-- flag = 3, -- number of the flag
-- stopflag = 4, -- optional number of the stop flag
-- req_num = 10, -- optional minimum amount of map objects needed to die
-- }
-- mist.flagFuncs.mapobjs_dead_zones(vars)
-- @tparam table vars table containing parameters.
function mist.flagFunc.mapobjs_dead_zones(vars)
--[[vars needs to be:
zones = table or string,
flag = number,
stopflag = number or nil,
req_num = number or nil
AND used by function,
initial_number
]]
-- type_tbl
local type_tbl = {
[{'zones', 'zone'}] = {'table', 'string'},
flag = {'number', 'string'},
[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
[{'req_num', 'reqnum'}] = {'number', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_zones', type_tbl, vars)
assert(err, errmsg)
local zones = vars.zones or vars.zone
local flag = vars.flag
local stopflag = vars.stopflag or vars.stopFlag or -1
local req_num = vars.req_num or vars.reqnum or 1
local initial_number = vars.initial_number
if type(zones) == 'string' then
zones = {zones}
end
if not initial_number then
initial_number = #mist.getDeadMapObjsInZones(zones)
end
if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
if (#mist.getDeadMapObjsInZones(zones) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
return
else
mist.scheduleFunction(mist.flagFunc.mapobjs_dead_zones, {{zones = zones, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1)
end
end
end
--- Sets a flag if map objects are destroyed inside a polygon.
-- Once this function is run, it will start a continuously evaluated process
-- that will set a flag true if map objects (such as bridges, buildings in
-- town, etc.) die (or have died) in a polygon.
-- This will only happen once; once the flag is set true, the process ends.
-- @usage
-- -- Example vars table
-- vars = {
-- zone = {
-- [1] = mist.DBs.unitsByName['NE corner'].point,
-- [2] = mist.DBs.unitsByName['SE corner'].point,
-- [3] = mist.DBs.unitsByName['SW corner'].point,
-- [4] = mist.DBs.unitsByName['NW corner'].point
-- }
-- flag = 3, -- number of the flag
-- stopflag = 4, -- optional number of the stop flag
-- req_num = 10, -- optional minimum amount of map objects needed to die
-- }
-- mist.flagFuncs.mapobjs_dead_zones(vars)
-- @tparam table vars table containing parameters.
function mist.flagFunc.mapobjs_dead_polygon(vars)
--[[vars needs to be:
zone = table,
flag = number,
stopflag = number or nil,
req_num = number or nil
AND used by function,
initial_number
]]
-- type_tbl
local type_tbl = {
[{'zone', 'polyzone'}] = 'table',
flag = {'number', 'string'},
[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
[{'req_num', 'reqnum'}] = {'number', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_polygon', type_tbl, vars)
assert(err, errmsg)
local zone = vars.zone or vars.polyzone
local flag = vars.flag
local stopflag = vars.stopflag or vars.stopFlag or -1
local req_num = vars.req_num or vars.reqnum or 1
local initial_number = vars.initial_number
if not initial_number then
initial_number = #mist.getDeadMapObjsInPolygonZone(zone)
end
if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
if (#mist.getDeadMapObjsInPolygonZone(zone) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
return
else
mist.scheduleFunction(mist.flagFunc.mapobjs_dead_polygon, {{zone = zone, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1)
end
end
end
--- Sets a flag if unit(s) is/are inside a polygon.
-- @tparam table vars @{unitsInPolygonVars}
-- @usage -- set flag 11 to true as soon as any blue vehicles
-- -- are inside the polygon shape created off of the waypoints
-- -- of the group forest1
-- mist.flagFunc.units_in_polygon {
-- units = {'[blue][vehicle]'},
-- zone = mist.getGroupPoints('forest1'),
-- flag = 11
-- }
function mist.flagFunc.units_in_polygon(vars)
--[[vars needs to be:
units = table,
zone = table,
flag = number,
stopflag = number or nil,
maxalt = number or nil,
interval = number or nil,
req_num = number or nil
toggle = boolean or nil
unitTableDef = table or nil
]]
-- type_tbl
local type_tbl = {
[{'units', 'unit'}] = 'table',
[{'zone', 'polyzone'}] = 'table',
flag = {'number', 'string'},
[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
[{'maxalt', 'alt'}] = {'number', 'nil'},
interval = {'number', 'nil'},
[{'req_num', 'reqnum'}] = {'number', 'nil'},
toggle = {'boolean', 'nil'},
unitTableDef = {'table', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_polygon', type_tbl, vars)
assert(err, errmsg)
local units = vars.units or vars.unit
local zone = vars.zone or vars.polyzone
local flag = vars.flag
local stopflag = vars.stopflag or vars.stopFlag or -1
local interval = vars.interval or 1
local maxalt = vars.maxalt or vars.alt
local req_num = vars.req_num or vars.reqnum or 1
local toggle = vars.toggle or nil
local unitTableDef = vars.unitTableDef
if not units.processed then
unitTableDef = mist.utils.deepCopy(units)
end
if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts
if unitTableDef then
units = mist.makeUnitTable(unitTableDef)
end
end
if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then
local num_in_zone = 0
for i = 1, #units do
local unit = Unit.getByName(units[i]) or StaticObject.getByName(units[i])
if unit and unit:isExist() == true then
local pos = unit:getPosition().p
if mist.pointInPolygon(pos, zone, maxalt) then
num_in_zone = num_in_zone + 1
if num_in_zone >= req_num and trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
break
end
end
end
end
if toggle and (num_in_zone < req_num) and trigger.misc.getUserFlag(flag) > 0 then
trigger.action.setUserFlag(flag, false)
end
-- do another check in case stopflag was set true by this function
if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then
mist.scheduleFunction(mist.flagFunc.units_in_polygon, {{units = units, zone = zone, flag = flag, stopflag = stopflag, interval = interval, req_num = req_num, maxalt = maxalt, toggle = toggle, unitTableDef = unitTableDef}}, timer.getTime() + interval)
end
end
end
--- Sets a flag if unit(s) is/are inside a trigger zone.
-- @todo document
function mist.flagFunc.units_in_zones(vars)
--[[vars needs to be:
units = table,
zones = table,
flag = number,
stopflag = number or nil,
zone_type = string or nil,
req_num = number or nil,
interval = number or nil
toggle = boolean or nil
]]
-- type_tbl
local type_tbl = {
units = 'table',
zones = 'table',
flag = {'number', 'string'},
[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
[{'zone_type', 'zonetype'}] = {'string', 'nil'},
[{'req_num', 'reqnum'}] = {'number', 'nil'},
interval = {'number', 'nil'},
toggle = {'boolean', 'nil'},
unitTableDef = {'table', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_zones', type_tbl, vars)
assert(err, errmsg)
local units = vars.units
local zones = vars.zones
local flag = vars.flag
local stopflag = vars.stopflag or vars.stopFlag or -1
local zone_type = vars.zone_type or vars.zonetype or 'cylinder'
local req_num = vars.req_num or vars.reqnum or 1
local interval = vars.interval or 1
local toggle = vars.toggle or nil
local unitTableDef = vars.unitTableDef
if not units.processed then
unitTableDef = mist.utils.deepCopy(units)
end
if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts
if unitTableDef then
units = mist.makeUnitTable(unitTableDef)
end
end
if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
local in_zone_units = mist.getUnitsInZones(units, zones, zone_type)
if #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
elseif #in_zone_units < req_num and toggle then
trigger.action.setUserFlag(flag, false)
end
-- do another check in case stopflag was set true by this function
if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
mist.scheduleFunction(mist.flagFunc.units_in_zones, {{units = units, zones = zones, flag = flag, stopflag = stopflag, zone_type = zone_type, req_num = req_num, interval = interval, toggle = toggle, unitTableDef = unitTableDef}}, timer.getTime() + interval)
end
end
end
--[[
function mist.flagFunc.weapon_in_zones(vars)
-- borrow from suchoi surprise. While running enabled event handler that checks for weapons in zone.
-- Choice is weapon category or weapon strings.
end
]]
--- Sets a flag if unit(s) is/are inside a moving zone.
-- @todo document
function mist.flagFunc.units_in_moving_zones(vars)
--[[vars needs to be:
units = table,
zone_units = table,
radius = number,
flag = number,
stopflag = number or nil,
zone_type = string or nil,
req_num = number or nil,
interval = number or nil
toggle = boolean or nil
]]
-- type_tbl
local type_tbl = {
units = 'table',
[{'zone_units', 'zoneunits'}] = 'table',
radius = 'number',
flag = {'number', 'string'},
[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
[{'zone_type', 'zonetype'}] = {'string', 'nil'},
[{'req_num', 'reqnum'}] = {'number', 'nil'},
interval = {'number', 'nil'},
toggle = {'boolean', 'nil'},
unitTableDef = {'table', 'nil'},
zUnitTableDef = {'table', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_moving_zones', type_tbl, vars)
assert(err, errmsg)
local units = vars.units
local zone_units = vars.zone_units or vars.zoneunits
local radius = vars.radius
local flag = vars.flag
local stopflag = vars.stopflag or vars.stopFlag or -1
local zone_type = vars.zone_type or vars.zonetype or 'cylinder'
local req_num = vars.req_num or vars.reqnum or 1
local interval = vars.interval or 1
local toggle = vars.toggle or nil
local unitTableDef = vars.unitTableDef
local zUnitTableDef = vars.zUnitTableDef
if not units.processed then
unitTableDef = mist.utils.deepCopy(units)
end
if not zone_units.processed then
zUnitTableDef = mist.utils.deepCopy(zone_units)
end
if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts
if unitTableDef then
units = mist.makeUnitTable(unitTableDef)
end
end
if (zone_units.processed and zone_units.processed < mist.getLastDBUpdateTime()) or not zone_units.processed then -- run unit table short cuts
if zUnitTableDef then
zone_units = mist.makeUnitTable(zUnitTableDef)
end
end
if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
local in_zone_units = mist.getUnitsInMovingZones(units, zone_units, radius, zone_type)
if #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
elseif #in_zone_units < req_num and toggle then
trigger.action.setUserFlag(flag, false)
end
-- do another check in case stopflag was set true by this function
if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
mist.scheduleFunction(mist.flagFunc.units_in_moving_zones, {{units = units, zone_units = zone_units, radius = radius, flag = flag, stopflag = stopflag, zone_type = zone_type, req_num = req_num, interval = interval, toggle = toggle, unitTableDef = unitTableDef, zUnitTableDef = zUnitTableDef}}, timer.getTime() + interval)
end
end
end
--- Sets a flag if units have line of sight to each other.
-- @todo document
function mist.flagFunc.units_LOS(vars)
--[[vars needs to be:
unitset1 = table,
altoffset1 = number,
unitset2 = table,
altoffset2 = number,
flag = number,
stopflag = number or nil,
radius = number or nil,
interval = number or nil,
req_num = number or nil
toggle = boolean or nil
]]
-- type_tbl
local type_tbl = {
[{'unitset1', 'units1'}] = 'table',
[{'altoffset1', 'alt1'}] = 'number',
[{'unitset2', 'units2'}] = 'table',
[{'altoffset2', 'alt2'}] = 'number',
flag = {'number', 'string'},
[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
[{'req_num', 'reqnum'}] = {'number', 'nil'},
interval = {'number', 'nil'},
radius = {'number', 'nil'},
toggle = {'boolean', 'nil'},
unitTableDef1 = {'table', 'nil'},
unitTableDef2 = {'table', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_LOS', type_tbl, vars)
assert(err, errmsg)
local unitset1 = vars.unitset1 or vars.units1
local altoffset1 = vars.altoffset1 or vars.alt1
local unitset2 = vars.unitset2 or vars.units2
local altoffset2 = vars.altoffset2 or vars.alt2
local flag = vars.flag
local stopflag = vars.stopflag or vars.stopFlag or -1
local interval = vars.interval or 1
local radius = vars.radius or math.huge
local req_num = vars.req_num or vars.reqnum or 1
local toggle = vars.toggle or nil
local unitTableDef1 = vars.unitTableDef1
local unitTableDef2 = vars.unitTableDef2
if not unitset1.processed then
unitTableDef1 = mist.utils.deepCopy(unitset1)
end
if not unitset2.processed then
unitTableDef2 = mist.utils.deepCopy(unitset2)
end
if (unitset1.processed and unitset1.processed < mist.getLastDBUpdateTime()) or not unitset1.processed then -- run unit table short cuts
if unitTableDef1 then
unitset1 = mist.makeUnitTable(unitTableDef1)
end
end
if (unitset2.processed and unitset2.processed < mist.getLastDBUpdateTime()) or not unitset2.processed then -- run unit table short cuts
if unitTableDef2 then
unitset2 = mist.makeUnitTable(unitTableDef2)
end
end
if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
local unitLOSdata = mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)
if #unitLOSdata >= req_num and trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
elseif #unitLOSdata < req_num and toggle then
trigger.action.setUserFlag(flag, false)
end
-- do another check in case stopflag was set true by this function
if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
mist.scheduleFunction(mist.flagFunc.units_LOS, {{unitset1 = unitset1, altoffset1 = altoffset1, unitset2 = unitset2, altoffset2 = altoffset2, flag = flag, stopflag = stopflag, radius = radius, req_num = req_num, interval = interval, toggle = toggle, unitTableDef1 = unitTableDef1, unitTableDef2 = unitTableDef2}}, timer.getTime() + interval)
end
end
end
--- Sets a flag if group is alive.
-- @todo document
function mist.flagFunc.group_alive(vars)
--[[vars
groupName
flag
toggle
interval
stopFlag
]]
local type_tbl = {
[{'group', 'groupname', 'gp', 'groupName'}] = 'string',
flag = {'number', 'string'},
[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
interval = {'number', 'nil'},
toggle = {'boolean', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive', type_tbl, vars)
assert(err, errmsg)
local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname
local flag = vars.flag
local stopflag = vars.stopflag or vars.stopFlag or -1
local interval = vars.interval or 1
local toggle = vars.toggle or nil
if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true and #Group.getByName(groupName):getUnits() > 0 then
if trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
end
else
if toggle then
trigger.action.setUserFlag(flag, false)
end
end
end
if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
mist.scheduleFunction(mist.flagFunc.group_alive, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval)
end
end
--- Sets a flag if group is dead.
-- @todo document
function mist.flagFunc.group_dead(vars)
local type_tbl = {
[{'group', 'groupname', 'gp', 'groupName'}] = 'string',
flag = {'number', 'string'},
[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
interval = {'number', 'nil'},
toggle = {'boolean', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_dead', type_tbl, vars)
assert(err, errmsg)
local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname
local flag = vars.flag
local stopflag = vars.stopflag or vars.stopFlag or -1
local interval = vars.interval or 1
local toggle = vars.toggle or nil
if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
if (Group.getByName(groupName) and Group.getByName(groupName):isExist() == false) or (Group.getByName(groupName) and #Group.getByName(groupName):getUnits() < 1) or not Group.getByName(groupName) then
if trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
end
else
if toggle then
trigger.action.setUserFlag(flag, false)
end
end
end
if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
mist.scheduleFunction(mist.flagFunc.group_dead, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval)
end
end
--- Sets a flag if less than given percent of group is alive.
-- @todo document
function mist.flagFunc.group_alive_less_than(vars)
local type_tbl = {
[{'group', 'groupname', 'gp', 'groupName'}] = 'string',
percent = 'number',
flag = {'number', 'string'},
[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
interval = {'number', 'nil'},
toggle = {'boolean', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_less_than', type_tbl, vars)
assert(err, errmsg)
local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname
local flag = vars.flag
local percent = vars.percent
local stopflag = vars.stopflag or vars.stopFlag or -1
local interval = vars.interval or 1
local toggle = vars.toggle or nil
if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then
if Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() < percent/100 then
if trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
end
else
if toggle then
trigger.action.setUserFlag(flag, false)
end
end
else
if trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
end
end
end
if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
mist.scheduleFunction(mist.flagFunc.group_alive_less_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval)
end
end
--- Sets a flag if more than given percent of group is alive.
-- @todo document
function mist.flagFunc.group_alive_more_than(vars)
local type_tbl = {
[{'group', 'groupname', 'gp', 'groupName'}] = 'string',
percent = 'number',
flag = {'number', 'string'},
[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},
interval = {'number', 'nil'},
toggle = {'boolean', 'nil'},
}
local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_more_than', type_tbl, vars)
assert(err, errmsg)
local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname
local flag = vars.flag
local percent = vars.percent
local stopflag = vars.stopflag or vars.stopFlag or -1
local interval = vars.interval or 1
local toggle = vars.toggle or nil
if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then
if Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() > percent/100 then
if trigger.misc.getUserFlag(flag) == 0 then
trigger.action.setUserFlag(flag, true)
end
else
if toggle and trigger.misc.getUserFlag(flag) == 1 then
trigger.action.setUserFlag(flag, false)
end
end
else --- just in case
if toggle and trigger.misc.getUserFlag(flag) == 1 then
trigger.action.setUserFlag(flag, false)
end
end
end
if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then
mist.scheduleFunction(mist.flagFunc.group_alive_more_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval)
end
end
mist.flagFunc.mapobjsDeadPolygon = mist.flagFunc.mapobjs_dead_polygon
mist.flagFunc.mapobjsDeadZones = mist.flagFunc.Mapobjs_dead_zones
mist.flagFunc.unitsInZones = mist.flagFunc.units_in_zones
mist.flagFunc.unitsInMovingZones = mist.flagFunc.units_in_moving_zones
mist.flagFunc.unitsInPolygon = mist.flagFunc.units_in_polygon
mist.flagFunc.unitsLOS = mist.flagFunc.units_LOS
mist.flagFunc.groupAlive = mist.flagFunc.group_alive
mist.flagFunc.groupDead = mist.flagFunc.group_dead
mist.flagFunc.groupAliveMoreThan = mist.flagFunc.group_alive_more_than
mist.flagFunc.groupAliveLessThan = mist.flagFunc.group_alive_less_than
end
--- Message functions.
-- Messaging system
-- @section mist.msg
do -- mist.msg scope
local messageList = {}
-- this defines the max refresh rate of the message box it honestly only needs to
-- go faster than this for precision timing stuff (which could be its own function)
local messageDisplayRate = 0.1
local messageID = 0
local displayActive = false
local displayFuncId = 0
local caSlots = false
local caMSGtoGroup = false
local anyUpdate = false
local anySound = false
local lastMessageTime = math.huge
if env.mission.groundControl then -- just to be sure?
for index, value in pairs(env.mission.groundControl) do
if type(value) == 'table' then
for roleName, roleVal in pairs(value) do
for rIndex, rVal in pairs(roleVal) do
if type(rVal) == 'number' and rVal > 0 then
caSlots = true
break
end
end
end
elseif type(value) == 'boolean' and value == true then
caSlots = true
break
end
end
end
local function mistdisplayV5()
log:warn("mistdisplayV5: $1", timer.getTime())
local clearView = true
if #messageList > 0 then
log:warn('Updates: $1', anyUpdate)
if anyUpdate == true or anySound == true then
local activeClients = {}
for clientId, clientData in pairs(mist.DBs.humansById) do
if Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then
activeClients[clientData.groupId] = clientData.groupName
end
end
if displayActive == false then
displayActive = true
end
--mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua')
local msgTableText = {}
local msgTableSound = {}
local curTime = timer.getTime()
for mInd, messageData in pairs(messageList) do
log:warn(messageData)
if messageData.displayTill < curTime then
log:warn('remove')
messageData:remove() -- now using the remove/destroy function.
else
if messageData.displayedFor then
messageData.displayedFor = curTime - messageData.addedAt
end
local soundIndex = 0
local refSound = 100000
if messageData.multSound and #messageData.multSound > 0 then
anySound = true
for index, sData in pairs(messageData.multSound) do
if sData.time <= messageData.displayedFor and sData.played == false and sData.time < refSound then -- find index of the next sound to be played
refSound = sData.time
soundIndex = index
end
end
if soundIndex ~= 0 then
messageData.multSound[soundIndex].played = true
end
end
for recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants
if recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists
if messageData.text then -- text
if not msgTableText[recData] then -- create table entry for text
msgTableText[recData] = {}
msgTableText[recData].text = {}
if recData == 'RED' or recData == 'BLUE' then
msgTableText[recData].text[1] = '-------Combined Arms Message-------- \n'
end
msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text
msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor
else -- add to table entry and adjust display time if needed
if recData == 'RED' or recData == 'BLUE' then
msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- Combined Arms Message: \n'
else
msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- \n'
end
table.insert(msgTableText[recData].text, messageData.text)
if msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then
msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor
else
--msgTableText[recData].displayTime = 10
end
end
end
if soundIndex ~= 0 then
msgTableSound[recData] = messageData.multSound[soundIndex].file
end
end
end
messageData.update = nil
end
end
------- new display
if anyUpdate == true then
if caSlots == true and caMSGtoGroup == false then
if msgTableText.RED then
trigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, clearView)
end
if msgTableText.BLUE then
trigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, clearView)
end
end
for index, msgData in pairs(msgTableText) do
if type(index) == 'number' then -- its a groupNumber
trigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, clearView)
end
end
end
--- new audio
if msgTableSound.RED then
trigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED)
end
if msgTableSound.BLUE then
trigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE)
end
for index, file in pairs(msgTableSound) do
if type(index) == 'number' then -- its a groupNumber
trigger.action.outSoundForGroup(index, file)
end
end
end
anyUpdate = false
anySound = false
else
mist.removeFunction(displayFuncId)
displayActive = false
end
end
local function mistdisplayV4()
local activeClients = {}
for clientId, clientData in pairs(mist.DBs.humansById) do
if Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then
activeClients[clientData.groupId] = clientData.groupName
end
end
--[[if caSlots == true and caMSGtoGroup == true then
end]]
if #messageList > 0 then
if displayActive == false then
displayActive = true
end
--mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua')
local msgTableText = {}
local msgTableSound = {}
for messageId, messageData in pairs(messageList) do
if messageData.displayedFor > messageData.displayTime then
messageData:remove() -- now using the remove/destroy function.
else
if messageData.displayedFor then
messageData.displayedFor = messageData.displayedFor + messageDisplayRate
end
local nextSound = 1000
local soundIndex = 0
if messageData.multSound and #messageData.multSound > 0 then
for index, sData in pairs(messageData.multSound) do
if sData.time <= messageData.displayedFor and sData.played == false and sData.time < nextSound then -- find index of the next sound to be played
nextSound = sData.time
soundIndex = index
end
end
if soundIndex ~= 0 then
messageData.multSound[soundIndex].played = true
end
end
for recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants
if recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists
if messageData.text then -- text
if not msgTableText[recData] then -- create table entry for text
msgTableText[recData] = {}
msgTableText[recData].text = {}
if recData == 'RED' or recData == 'BLUE' then
msgTableText[recData].text[1] = '-------Combined Arms Message-------- \n'
end
msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text
msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor
else -- add to table entry and adjust display time if needed
if recData == 'RED' or recData == 'BLUE' then
msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- Combined Arms Message: \n'
else
msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- \n'
end
msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text
if msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then
msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor
else
msgTableText[recData].displayTime = 1
end
end
end
if soundIndex ~= 0 then
msgTableSound[recData] = messageData.multSound[soundIndex].file
end
end
end
end
end
------- new display
if caSlots == true and caMSGtoGroup == false then
if msgTableText.RED then
trigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, true)
end
if msgTableText.BLUE then
trigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, true)
end
end
for index, msgData in pairs(msgTableText) do
if type(index) == 'number' then -- its a groupNumber
trigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, true)
end
end
--- new audio
if msgTableSound.RED then
trigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED)
end
if msgTableSound.BLUE then
trigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE)
end
for index, file in pairs(msgTableSound) do
if type(index) == 'number' then -- its a groupNumber
trigger.action.outSoundForGroup(index, file)
end
end
else
mist.removeFunction(displayFuncId)
displayActive = false
end
end
local typeBase = {
['Mi-8MT'] = {'Mi-8MTV2', 'Mi-8MTV', 'Mi-8'},
['MiG-21Bis'] = {'Mig-21'},
['MiG-15bis'] = {'Mig-15'},
['FW-190D9'] = {'FW-190'},
['Bf-109K-4'] = {'Bf-109'},
}
--[[function mist.setCAGroupMSG(val)
if type(val) == 'boolean' then
caMSGtoGroup = val
return true
end
return false
end]]
mist.message = {
add = function(vars)
local function msgSpamFilter(recList, spamBlockOn)
for id, name in pairs(recList) do
if name == spamBlockOn then
-- log:info('already on recList')
return recList
end
end
--log:info('add to recList')
table.insert(recList, spamBlockOn)
return recList
end
--[[
local vars = {}
vars.text = 'Hello World'
vars.displayTime = 20
vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}}
mist.message.add(vars)
Displays the message for all red coalition players. Players belonging to Ukraine and Georgia, and all A-10Cs on the map
]]
local new = {}
new.text = vars.text -- The actual message
new.displayTime = vars.displayTime -- How long will the message appear for
new.displayedFor = 0 -- how long the message has been displayed so far
new.displayTill = timer.getTime() + vars.displayTime
new.name = vars.name -- ID to overwrite the older message (if it exists) Basically it replaces a message that is displayed with new text.
new.addedAt = timer.getTime()
new.clearView = vars.clearView or true
--log:warn('New Message: $1', new.text)
if vars.multSound and vars.multSound[1] then
new.multSound = vars.multSound
else
new.multSound = {}
end
if vars.sound or vars.fileName then -- converts old sound file system into new multSound format
local sound = vars.sound
if vars.fileName then
sound = vars.fileName
end
new.multSound[#new.multSound+1] = {time = 0.1, file = sound}
end
if #new.multSound > 0 then
for i, data in pairs(new.multSound) do
data.played = false
end
end
local newMsgFor = {} -- list of all groups message displays for
for forIndex, forData in pairs(vars.msgFor) do
for list, listData in pairs(forData) do
for clientId, clientData in pairs(mist.DBs.humansById) do
forIndex = string.lower(forIndex)
if type(listData) == 'string' then
listData = string.lower(listData)
end
if (forIndex == 'coa' and (listData == string.lower(clientData.coalition) or listData == 'all')) or (forIndex == 'countries' and string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then --
newMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- so units dont get the same message twice if complex rules are given
--table.insert(newMsgFor, clientId)
elseif forIndex == 'unittypes' then
for typeId, typeData in pairs(listData) do
local found = false
for clientDataEntry, clientDataVal in pairs(clientData) do
if type(clientDataVal) == 'string' then
if mist.matchString(list, clientDataVal) == true or list == 'all' then
local sString = typeData
for rName, pTbl in pairs(typeBase) do -- just a quick check to see if the user may have meant something and got the specific type of the unit wrong
for pIndex, pName in pairs(pTbl) do
if mist.stringMatch(sString, pName) then
sString = rName
end
end
end
if sString == clientData.type then
found = true
newMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- sends info oto other function to see if client is already recieving the current message.
--table.insert(newMsgFor, clientId)
end
end
end
if found == true then -- shouldn't this be elsewhere too?
break
end
end
end
end
end
for coaData, coaId in pairs(coalition.side) do
if string.lower(forIndex) == 'coa' or string.lower(forIndex) == 'ca' then
if listData == string.lower(coaData) or listData == 'all' then
newMsgFor = msgSpamFilter(newMsgFor, coaData)
end
end
end
end
end
if #newMsgFor > 0 then
new.msgFor = newMsgFor -- I swear its not confusing
else
return false
end
if vars.name and type(vars.name) == 'string' then
for i = 1, #messageList do
if messageList[i].name then
if messageList[i].name == vars.name then
--log:info('updateMessage')
messageList[i].displayTill = timer.getTime() + messageList[i].displayTime
messageList[i].displayedFor = 0
messageList[i].addedAt = timer.getTime()
messageList[i].text = new.text
messageList[i].msgFor = new.msgFor
messageList[i].multSound = new.multSound
anyUpdate = true
--log:warn('Message updated: $1', new.messageID)
return messageList[i].messageID
end
end
end
end
anyUpdate = true
messageID = messageID + 1
new.messageID = messageID
--mist.debug.writeData(mist.utils.serialize,{'msg', new}, 'newMsg.lua')
messageList[#messageList + 1] = new
local mt = { __index = mist.message}
setmetatable(new, mt)
if displayActive == false then
displayActive = true
displayFuncId = mist.scheduleFunction(mistdisplayV4, {}, timer.getTime() + messageDisplayRate, messageDisplayRate)
end
return messageID
end,
remove = function(self) -- Now a self variable; the former functionality taken up by mist.message.removeById.
for i, msgData in pairs(messageList) do
if messageList[i] == self then
table.remove(messageList, i)
anyUpdate = true
return true --removal successful
end
end
return false -- removal not successful this script fails at life!
end,
removeById = function(id) -- This function is NOT passed a self variable; it is the remove by id function.
for i, msgData in pairs(messageList) do
if messageList[i].messageID == id then
table.remove(messageList, i)
anyUpdate = true
return true --removal successful
end
end
return false -- removal not successful this script fails at life!
end,
}
--[[ vars for mist.msgMGRS
vars.units - table of unit names (NOT unitNameTable- maybe this should change).
vars.acc - integer between 0 and 5, inclusive
vars.text - text in the message
vars.displayTime - self explanatory
vars.msgFor - scope
]]
function mist.msgMGRS(vars)
local units = vars.units
local acc = vars.acc
local text = vars.text
local displayTime = vars.displayTime
local msgFor = vars.msgFor
local s = mist.getMGRSString{units = units, acc = acc}
local newText
if text then
if string.find(text, '%%s') then -- look for %s
newText = string.format(text, s) -- insert the coordinates into the message
else
-- just append to the end.
newText = text .. s
end
else
newText = s
end
mist.message.add{
text = newText,
displayTime = displayTime,
msgFor = msgFor
}
end
--[[ vars for mist.msgLL
vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes).
vars.acc - integer, number of numbers after decimal place
vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes.
vars.text - text in the message
vars.displayTime - self explanatory
vars.msgFor - scope
]]
function mist.msgLL(vars)
local units = vars.units -- technically, I don't really need to do this, but it helps readability.
local acc = vars.acc
local DMS = vars.DMS
local text = vars.text
local displayTime = vars.displayTime
local msgFor = vars.msgFor
local s = mist.getLLString{units = units, acc = acc, DMS = DMS}
local newText
if text then
if string.find(text, '%%s') then -- look for %s
newText = string.format(text, s) -- insert the coordinates into the message
else
-- just append to the end.
newText = text .. s
end
else
newText = s
end
mist.message.add{
text = newText,
displayTime = displayTime,
msgFor = msgFor
}
end
--[[
vars.units- table of unit names (NOT unitNameTable- maybe this should change).
vars.ref - vec3 ref point, maybe overload for vec2 as well?
vars.alt - boolean, if used, includes altitude in string
vars.metric - boolean, gives distance in km instead of NM.
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
function mist.msgBR(vars)
local units = vars.units -- technically, I don't really need to do this, but it helps readability.
local ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString
local alt = vars.alt
local metric = vars.metric
local text = vars.text
local displayTime = vars.displayTime
local msgFor = vars.msgFor
local s = mist.getBRString{units = units, ref = ref, alt = alt, metric = metric}
local newText
if text then
if string.find(text, '%%s') then -- look for %s
newText = string.format(text, s) -- insert the coordinates into the message
else
-- just append to the end.
newText = text .. s
end
else
newText = s
end
mist.message.add{
text = newText,
displayTime = displayTime,
msgFor = msgFor
}
end
-- basically, just sub-types of mist.msgBR... saves folks the work of getting the ref point.
--[[
vars.units- table of unit names (NOT unitNameTable- maybe this should change).
vars.ref - string red, blue
vars.alt - boolean, if used, includes altitude in string
vars.metric - boolean, gives distance in km instead of NM.
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
function mist.msgBullseye(vars)
if mist.DBs.missionData.bullseye[string.lower(vars.ref)] then
vars.ref = mist.DBs.missionData.bullseye[string.lower(vars.ref)]
mist.msgBR(vars)
end
end
--[[
vars.units- table of unit names (NOT unitNameTable- maybe this should change).
vars.ref - unit name of reference point
vars.alt - boolean, if used, includes altitude in string
vars.metric - boolean, gives distance in km instead of NM.
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
function mist.msgBRA(vars)
if Unit.getByName(vars.ref) and Unit.getByName(vars.ref):isExist() == true then
vars.ref = Unit.getByName(vars.ref):getPosition().p
if not vars.alt then
vars.alt = true
end
mist.msgBR(vars)
end
end
--[[ vars for mist.msgLeadingMGRS:
vars.units - table of unit names
vars.heading - direction
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees (optional)
vars.acc - number, 0 to 5.
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
function mist.msgLeadingMGRS(vars)
local units = vars.units -- technically, I don't really need to do this, but it helps readability.
local heading = vars.heading
local radius = vars.radius
local headingDegrees = vars.headingDegrees
local acc = vars.acc
local text = vars.text
local displayTime = vars.displayTime
local msgFor = vars.msgFor
local s = mist.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc}
local newText
if text then
if string.find(text, '%%s') then -- look for %s
newText = string.format(text, s) -- insert the coordinates into the message
else
-- just append to the end.
newText = text .. s
end
else
newText = s
end
mist.message.add{
text = newText,
displayTime = displayTime,
msgFor = msgFor
}
end
--[[ vars for mist.msgLeadingLL:
vars.units - table of unit names
vars.heading - direction, number
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees (optional)
vars.acc - number of digits after decimal point (can be negative)
vars.DMS - boolean, true if you want DMS. (optional)
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
function mist.msgLeadingLL(vars)
local units = vars.units -- technically, I don't really need to do this, but it helps readability.
local heading = vars.heading
local radius = vars.radius
local headingDegrees = vars.headingDegrees
local acc = vars.acc
local DMS = vars.DMS
local text = vars.text
local displayTime = vars.displayTime
local msgFor = vars.msgFor
local s = mist.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS}
local newText
if text then
if string.find(text, '%%s') then -- look for %s
newText = string.format(text, s) -- insert the coordinates into the message
else
-- just append to the end.
newText = text .. s
end
else
newText = s
end
mist.message.add{
text = newText,
displayTime = displayTime,
msgFor = msgFor
}
end
--[[
vars.units - table of unit names
vars.heading - direction, number
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees (optional)
vars.metric - boolean, if true, use km instead of NM. (optional)
vars.alt - boolean, if true, include altitude. (optional)
vars.ref - vec3/vec2 reference point.
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
function mist.msgLeadingBR(vars)
local units = vars.units -- technically, I don't really need to do this, but it helps readability.
local heading = vars.heading
local radius = vars.radius
local headingDegrees = vars.headingDegrees
local metric = vars.metric
local alt = vars.alt
local ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString
local text = vars.text
local displayTime = vars.displayTime
local msgFor = vars.msgFor
local s = mist.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref}
local newText
if text then
if string.find(text, '%%s') then -- look for %s
newText = string.format(text, s) -- insert the coordinates into the message
else
-- just append to the end.
newText = text .. s
end
else
newText = s
end
mist.message.add{
text = newText,
displayTime = displayTime,
msgFor = msgFor
}
end
end
--- Demo functions.
-- @section mist.demos
do -- mist.demos scope
mist.demos = {}
function mist.demos.printFlightData(unit)
if unit:isExist() then
local function printData(unit, prevVel, prevE, prevTime)
local angles = mist.getAttitude(unit)
if angles then
local Heading = angles.Heading
local Pitch = angles.Pitch
local Roll = angles.Roll
local Yaw = angles.Yaw
local AoA = angles.AoA
local ClimbAngle = angles.ClimbAngle
if not Heading then
Heading = 'NA'
else
Heading = string.format('%12.2f', mist.utils.toDegree(Heading))
end
if not Pitch then
Pitch = 'NA'
else
Pitch = string.format('%12.2f', mist.utils.toDegree(Pitch))
end
if not Roll then
Roll = 'NA'
else
Roll = string.format('%12.2f', mist.utils.toDegree(Roll))
end
local AoAplusYaw = 'NA'
if AoA and Yaw then
AoAplusYaw = string.format('%12.2f', mist.utils.toDegree((AoA^2 + Yaw^2)^0.5))
end
if not Yaw then
Yaw = 'NA'
else
Yaw = string.format('%12.2f', mist.utils.toDegree(Yaw))
end
if not AoA then
AoA = 'NA'
else
AoA = string.format('%12.2f', mist.utils.toDegree(AoA))
end
if not ClimbAngle then
ClimbAngle = 'NA'
else
ClimbAngle = string.format('%12.2f', mist.utils.toDegree(ClimbAngle))
end
local unitPos = unit:getPosition()
local unitVel = unit:getVelocity()
local curTime = timer.getTime()
local absVel = string.format('%12.2f', mist.vec.mag(unitVel))
local unitAcc = 'NA'
local Gs = 'NA'
local axialGs = 'NA'
local transGs = 'NA'
if prevVel and prevTime then
local xAcc = (unitVel.x - prevVel.x)/(curTime - prevTime)
local yAcc = (unitVel.y - prevVel.y)/(curTime - prevTime)
local zAcc = (unitVel.z - prevVel.z)/(curTime - prevTime)
unitAcc = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc, z = zAcc}))
Gs = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc + 9.81, z = zAcc})/9.81)
axialGs = string.format('%12.2f', mist.vec.dp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x)/9.81)
transGs = string.format('%12.2f', mist.vec.mag(mist.vec.cp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x))/9.81)
end
local E = 0.5*mist.vec.mag(unitVel)^2 + 9.81*unitPos.p.y
local energy = string.format('%12.2e', E)
local dEdt = 'NA'
if prevE and prevTime then
dEdt = string.format('%12.2e', (E - prevE)/(curTime - prevTime))
end
trigger.action.outText(string.format('%-25s', 'Heading: ') .. Heading .. ' degrees\n' .. string.format('%-25s', 'Roll: ') .. Roll .. ' degrees\n' .. string.format('%-25s', 'Pitch: ') .. Pitch
.. ' degrees\n' .. string.format('%-25s', 'Yaw: ') .. Yaw .. ' degrees\n' .. string.format('%-25s', 'AoA: ') .. AoA .. ' degrees\n' .. string.format('%-25s', 'AoA plus Yaw: ') .. AoAplusYaw .. ' degrees\n' .. string.format('%-25s', 'Climb Angle: ') ..
ClimbAngle .. ' degrees\n' .. string.format('%-25s', 'Absolute Velocity: ') .. absVel .. ' m/s\n' .. string.format('%-25s', 'Absolute Acceleration: ') .. unitAcc ..' m/s^2\n'
.. string.format('%-25s', 'Axial G loading: ') .. axialGs .. ' g\n' .. string.format('%-25s', 'Transverse G loading: ') .. transGs .. ' g\n' .. string.format('%-25s', 'Absolute G loading: ') .. Gs .. ' g\n' .. string.format('%-25s', 'Energy: ') .. energy .. ' J/kg\n' .. string.format('%-25s', 'dE/dt: ') .. dEdt ..' J/(kg*s)', 1)
return unitVel, E, curTime
end
end
local function frameFinder(unit, prevVel, prevE, prevTime)
if unit:isExist() then
local currVel = unit:getVelocity()
if prevVel and (prevVel.x ~= currVel.x or prevVel.y ~= currVel.y or prevVel.z ~= currVel.z) or (prevTime and (timer.getTime() - prevTime) > 0.25) then
prevVel, prevE, prevTime = printData(unit, prevVel, prevE, prevTime)
end
mist.scheduleFunction(frameFinder, {unit, prevVel, prevE, prevTime}, timer.getTime() + 0.005) -- it can't go this fast, limited to the 100 times a sec check right now.
end
end
local curVel = unit:getVelocity()
local curTime = timer.getTime()
local curE = 0.5*mist.vec.mag(curVel)^2 + 9.81*unit:getPosition().p.y
frameFinder(unit, curVel, curE, curTime)
end
end
end
do
--[[ stuff for marker panels
marker.add() add marker. Point of these functions is to simplify process and to store all mark panels added.
-- generates Id if not specified or if multiple marks created.
-- makes marks for countries by creating a mark for each client group in the country
-- can create multiple marks if needed for groups and countries.
-- adds marks to table for parsing and removing
-- Uses similar structure as messages. Big differences is it doesn't only mark to groups.
If to All, then mark is for All
if to coa mark is to coa
if to specific units, mark is to group
--------
STUFF TO Check
--------
If mark added to a group before a client joins slot is synced.
Mark made for cliet A in Slot A. Client A leaves, Client B joins in slot A. What do they see?
May need to automate process...
Could release this. But things I might need to add/change before doing so.
- removing marks and re-adding in same sequence doesn't appear to work. May need to schedule adding mark if updating an entry.
- I really dont like the old message style code for which groups get the message. Perhaps change to unitsTable and create function for getting humanUnitsTable.
= Event Handler, and check it, for marks added via script or user to deconflict Ids.
- Full validation of passed values for a specific shape type.
]]
local usedMarks = {}
local mDefs = {
coa = {
['red'] = {fillColor = {.8, 0 , 0, .5}, color = {.8, 0 , 0, .5}, lineType = 2, fontSize = 16},
['blue'] = {fillColor = {0, 0 , 0.8, .5}, color = {0, 0 , 0.8, .5}, lineType = 2, fontSize = 16},
['all'] = {fillColor = {.1, .1 , .1, .5}, color = {.9, .9 , .9, .5}, lineType = 2, fontSize = 16},
['neutral'] = {fillColor = {.1, .1 , .1, .5}, color = {.2, .2 , .2, .5}, lineType = 2, fontSize = 16},
},
}
local userDefs = {['red'] = {},['blue'] = {},['all'] = {},['neutral'] = {}}
local mId = 1000
local tNames = {'line', 'circle','rect', 'arrow', 'text', 'quad', 'freeform'}
local tLines = {[0] = 'no line', [1] = 'solid', [2] = 'dashed',[3] = 'dotted', [4] = 'dot dash' ,[5] = 'long dash', [6] = 'two dash'}
local coas = {[-1] = 'all', [0] = 'neutral', [1] = 'red', [2] = 'blue'}
local altNames = {['poly'] = 7, ['lines'] = 1, ['polygon'] = 7 }
local function draw(s)
--log:warn(s)
if type(s) == 'table' then
local mType = s.markType
--log:echo(s)
if mType == 'panel' then
local markScope = s.markScope or "all"
if markScope == 'coa' then
trigger.action.markToCoalition(s.markId, s.text, s.pos, s.markFor, s.readOnly)
elseif markScope == 'group' then
trigger.action.markToGroup(s.markId, s.text, s.pos, s.markFor, s.readOnly)
else
trigger.action.markToAll(s.markId, s.text, s.pos, s.readOnly)
end
elseif mType == 'line' then
trigger.action.lineToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message)
elseif mType == 'circle' then
trigger.action.circleToAll(s.coa, s.markId, s.pos[1], s.radius, s.color, s.fillColor, s.lineType, s.readOnly, s.message)
elseif mType == 'rect' then
trigger.action.rectToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message)
elseif mType == 'arrow' then
trigger.action.arrowToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message)
elseif mType == 'text' then
trigger.action.textToAll(s.coa, s.markId, s.pos[1], s.color, s.fillColor, s.fontSize, s.readOnly, s.text)
elseif mType == 'quad' then
trigger.action.quadToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.pos[3], s.pos[4], s.color, s.fillColor, s.lineType, s.readOnly, s.message)
end
if s.name and not usedMarks[s.name] then
usedMarks[s.name] = s.markId
end
elseif type(s) == 'string' then
--log:warn(s)
mist.utils.dostring(s)
end
end
mist.marker = {}
local function markSpamFilter(recList, spamBlockOn)
for id, name in pairs(recList) do
if name == spamBlockOn then
--log:info('already on recList')
return recList
end
end
--log:info('add to recList')
table.insert(recList, spamBlockOn)
return recList
end
local function iterate()
while mId < 10000000 do
if usedMarks[mId] then
mId = mId + 1
else
return mist.utils.deepCopy(mId)
end
end
return mist.utils.deepCopy(mId)
end
local function validateColor(val)
if type(val) == 'table' then
for i = 1, 4 do
if val[i] then
if type(val[i]) == 'number' and val[i] > 1 then
val[i] = val[i]/255 -- convert RGB values from 0-255 to 0-1 equivilent.
end
else
val[i] = 0.8
log:warn("index $1 of color to mist.marker.add was missing, defaulted to 0.8", i)
end
end
elseif type(val) == 'string' then
val = mist.utils.hexToRGB(val)
end
return val
end
local function checkDefs(vName, coa)
--log:warn('CheckDefs: $1 $2', vName, coa)
local coaName
if type(coa) == 'number' then
if coas[coa] then
coaName = coas[coa]
end
elseif type(coa) == 'string' then
coaName = coa
end
-- log:warn(coaName)
if userDefs[coaName] and userDefs[coaName][vName] then
return userDefs[coaName][vName]
elseif mDefs.coa[coaName] and mDefs.coa[coaName][vName] then
return mDefs.coa[coaName][vName]
end
end
function mist.marker.getNextId()
return iterate()
end
local handle = {}
function handle:onEvent(e)
if world.event.S_EVENT_MARK_ADDED == e.id and e.idx then
usedMarks[e.idx] = e.idx
if not mist.DBs.markList[e.idx] then
--log:info('create maker DB: $1', e.idx)
mist.DBs.markList[e.idx] = {time = e.time, pos = e.pos, groupId = e.groupId, mType = 'panel', text = e.text, markId = e.idx, coalition = e.coalition}
if e.unit then
mist.DBs.markList[e.idx].unit = e.initiator:getName()
end
--log:info(mist.marker.list[e.idx])
end
elseif world.event.S_EVENT_MARK_CHANGE == e.id and e.idx then
if mist.DBs.markList[e.idx] then
mist.DBs.markList[e.idx].text = e.text
end
elseif world.event.S_EVENT_MARK_REMOVE == e.id and e.idx then
if mist.DBs.markList[e.idx] then
mist.DBs.markList[e.idx] = nil
end
end
end
local function getMarkId(id)
if mist.DBs.markList[id] then
return id
else
for mEntry, mData in pairs(mist.DBs.markList) do
if id == mData.name or id == mData.id then
return mData.markId
end
end
end
end
local function removeMark(id)
--log:info("Removing Mark: $1", id)
local removed = false
if type(id) == 'table' then
for ind, val in pairs(id) do
local r
if val.markId then
r = val.markId
else
r = getMarkId(val)
end
if r then
trigger.action.removeMark(r)
mist.DBs.markList[r] = nil
removed = true
end
end
else
local r = getMarkId(id)
if r then
trigger.action.removeMark(r)
mist.DBs.markList[r] = nil
removed = true
end
end
return removed
end
world.addEventHandler(handle)
function mist.marker.setDefault(vars)
local anyChange = false
if vars and type(vars) == 'table' then
for l1, l1Data in pairs(vars) do
if type(l1Data) == 'table' then
if not userDefs[l1] then
userDefs[l1] = {}
end
for l2, l2Data in pairs(l1Data) do
userDefs[l1][l2] = l2Data
anyChange = true
end
else
userDefs[l1] = l1Data
anyChange = true
end
end
end
return anyChange
end
function mist.marker.add(vars)
--log:warn('markerFunc')
--log:warn(vars)
local pos = vars.point or vars.points or vars.pos
local text = vars.text or ''
local markFor = vars.markFor
local markForCoa = vars.markForCoa or vars.coa -- optional, can be used if you just want to mark to a specific coa/all
local id = vars.id or vars.markId or vars.markid
local mType = vars.mType or vars.markType or vars.type or 0
local color = vars.color
local fillColor = vars.fillColor
local lineType = vars.lineType or 2
local readOnly = vars.readOnly or true
local message = vars.message
local fontSize = vars.fontSize
local name = vars.name
local radius = vars.radius or 500
local coa = -1
local usedId = 0
pos = mist.utils.deepCopy(pos)
if id then
if type(id) ~= 'number' then
name = id
usedId = iterate()
end
--log:info('checkIfIdExist: $1', id)
--[[
Maybe it should treat id or name as the same thing/single value.
If passed number it will use that as the first Id used and will delete/update any marks associated with that same value.
]]
local lId = id or name
if mist.DBs.markList[id] then ---------- NEED A BETTER WAY TO ASSOCIATE THE ID VALUE. CUrrnetly deleting from table and checking if that deleted entry exists which it wont.
--log:warn('active mark to be removed: $1', id)
name = mist.DBs.markList[id].name or id
removeMark(id)
elseif usedMarks[id] then
--log:info('exists in usedMarks: $1', id)
removeMark(usedMarks[id])
elseif name and usedMarks[name] then
--log:info('exists in usedMarks: $1', name)
removeMark(usedMarks[name])
end
usedId = iterate()
usedMarks[id] = usedId -- redefine the value used
end
if name then
usedMarks[name] = usedId
end
if usedId == 0 then
usedId = iterate()
end
if mType then
if type(mType) == 'string' then
for i = 1, #tNames do
--log:warn(tNames[i])
if mist.stringMatch(mType, tNames[i]) then
mType = i
break
end
end
elseif type(mType) == 'number' and mType > #tNames then
mType = 0
end
end
--log:warn(mType)
local markScope = 'all'
local markForTable = {}
if pos then
if pos[1] then
for i = 1, #pos do
pos[i] = mist.utils.makeVec3(pos[i])
end
else
pos[1] = mist.utils.makeVec3(pos)
end
end
if text and type(text) ~= string then
text = tostring(text)
end
if markForCoa then
if type(markForCoa) == 'string' then
--log:warn("coa is string")
if tonumber(markForCoa) then
coa = coas[tonumber(markForCoa)]
markScope = 'coa'
else
for ind, cName in pairs(coas) do
if mist.stringMatch(cName, markForCoa) then
coa = ind
markScope = 'coa'
break
end
end
end
elseif type(markForCoa) == 'number' and markForCoa >=-1 and markForCoa <= #coas then
coa = markForCoa
--log:warn("coa is number")
markScope = 'coa'
end
markFor = coa
elseif markFor then
if type(markFor) == 'number' then -- groupId
if mist.DBs.groupsById[markFor] then
markScope = 'group'
end
elseif type(markFor) == 'string' then -- groupName
if mist.DBs.groupsByName[markFor] then
markScope = 'group'
markFor = mist.DBs.groupsByName[markFor].groupId
end
elseif type(markFor) == 'table' then -- multiple groupName, country, coalition, all
markScope = 'table'
--log:warn(markFor)
for forIndex, forData in pairs(markFor) do -- need to rethink this part and organization. Gotta be a more logical way to send messages to coa, groups, or all.
for list, listData in pairs(forData) do
--log:warn(listData)
forIndex = string.lower(forIndex)
if type(listData) == 'string' then
listData = string.lower(listData)
end
if listData == 'all' then
markScope = 'all'
break
elseif (forIndex == 'coa' or forIndex == 'ca') then -- mark for coa or CA.
local matches = 0
for name, index in pairs (coalition.side) do
if listData == string.lower(name) then
markScope = 'coa'
markFor = index
coa = index
matches = matches + 1
end
end
if matches > 1 then
markScope = 'all'
end
elseif forIndex == 'countries' then
for clienId, clientData in pairs(mist.DBs.humansById) do
if (string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then
markForTable = markSpamFilter(markForTable, clientData.groupId)
end
end
elseif forIndex == 'unittypes' then -- mark to group
-- iterate play units
for clientId, clientData in pairs(mist.DBs.humansById) do
for typeId, typeData in pairs(listData) do
--log:warn(typeData)
local found = false
if list == 'all' or clientData.coalition and type(clientData.coalition) == 'string' and mist.stringMatch(clientData.coalition, list) then
if mist.matchString(typeData, clientData.type) then
found = true
else
-- check other known names for aircraft
end
end
if found == true then
markForTable = markSpamFilter(markForTable, clientData.groupId) -- sends info to other function to see if client is already recieving the current message.
end
for clientDataEntry, clientDataVal in pairs(clientData) do
if type(clientDataVal) == 'string' then
if mist.matchString(list, clientDataVal) == true or list == 'all' then
local sString = typeData
for rName, pTbl in pairs(typeBase) do -- just a quick check to see if the user may have meant something and got the specific type of the unit wrong
for pIndex, pName in pairs(pTbl) do
if mist.stringMatch(sString, pName) then
sString = rName
end
end
end
if mist.stringMatch(sString, clientData.type) then
found = true
markForTable = markSpamFilter(markForTable, clientData.groupId) -- sends info oto other function to see if client is already recieving the current message.
--table.insert(newMsgFor, clientId)
end
end
end
if found == true then -- shouldn't this be elsewhere too?
break
end
end
end
end
end
end
end
end
else
markScope = 'all'
end
if mType == 0 then
local data = {markId = usedId, text = text, pos = pos[1], markScope = markScope, markFor = markFor, markType = 'panel', name = name, time = timer.getTime()}
if markScope ~= 'table' then
-- create marks
mist.DBs.markList[usedId] = data-- add to the DB
else
if #markForTable > 0 then
--log:info('iterate')
local list = {}
if id and not name then
name = id
end
for i = 1, #markForTable do
local newId = iterate()
local data = {markId = newId, text = text, pos = pos[i], markScope = markScope, markFor = markForTable[i], markType = 'panel', name = name, readOnly = readOnly, time = timer.getTime()}
mist.DBs.markList[newId] = data
table.insert(list, data)
draw(data)
end
return list
end
end
draw(data)
return data
elseif mType > 0 then
local newId = iterate()
local fCal = {}
fCal[#fCal+1] = mType
fCal[#fCal+1] = coa
fCal[#fCal+1] = usedId
local likeARainCoat = false
if mType == 7 then
local score = 0
for i = 1, #pos do
if i < #pos then
local val = ((pos[i+1].x - pos[i].x)*(pos[i+1].z + pos[i].z))
--log:warn("$1 index score is: $2", i, val)
score = score + val
else
score = score + ((pos[1].x - pos[i].x)*(pos[1].z + pos[i].z))
end
end
--log:warn(score)
if score > 0 then -- it is anti-clockwise. Due to DCS bug make it clockwise.
likeARainCoat = true
--log:warn('flip')
for i = #pos, 1, -1 do
fCal[#fCal+1] = pos[i]
end
end
end
if likeARainCoat == false then
for i = 1, #pos do
fCal[#fCal+1] = pos[i]
end
end
if radius and mType == 2 then
fCal[#fCal+1] = radius
end
if not color then
color = checkDefs('color', coa)
else
color = validateColor(color)
end
fCal[#fCal+1] = color
if not fillColor then
fillColor = checkDefs('fillColor', coa)
else
fillColor = validateColor(fillColor)
end
fCal[#fCal+1] = fillColor
if mType == 5 then -- text to all
if not fontSize then
fontSize = checkDefs('fontSize', coa) or 16
end
fCal[#fCal+1] = fontSize
else
if not lineType then
lineType = checkDefs('lineType', coa) or 2
end
end
fCal[#fCal+1] = lineType
if not readOnly then
readOnly = true
end
fCal[#fCal+1] = readOnly
if mType == 5 then
fCal[#fCal+1] = text
else
fCal[#fCal+1] = message
end
local data = {coa = coa, markId = usedId, pos = pos, markFor = markFor, color = color, readOnly = readOnly, message = message, fillColor = fillColor, lineType = lineType, markType = tNames[mType], name = name, radius = radius, text = text, fontSize = fontSize, time = timer.getTime()}
mist.DBs.markList[usedId] = data
if mType == 7 or mType == 1 then
local s = "trigger.action.markupToAll("
for i = 1, #fCal do
--log:warn(fCal[i])
if type(fCal[i]) == 'table' or type(fCal[i]) == 'boolean' then
s = s .. mist.utils.oneLineSerialize(fCal[i])
else
s = s .. fCal[i]
end
if i < #fCal then
s = s .. ','
end
end
s = s .. ')'
if name then
usedMarks[name] = usedId
end
draw(s)
else
draw(data)
end
return data
end
end
function mist.marker.remove(id)
return removeMark(id)
end
function mist.marker.get(id)
if mist.DBs.markList[id] then
return mist.DBs.markList[id]
end
local names = {}
for markId, data in pairs(mist.DBs.markList) do
if data.name and data.name == id then
table.insert(names, data)
end
end
if #names >= 1 then
return names
end
end
function mist.marker.drawZone(name, v)
if mist.DBs.zonesByName[name] then
--log:warn(mist.DBs.zonesByName[name])
local vars = v or {}
local ref = mist.utils.deepCopy(mist.DBs.zonesByName[name])
if ref.type == 2 then -- it is a quad, but use freeform cause it isnt as bugged
vars.mType = 6
vars.point = ref.verticies
else
vars.mType = 2
vars.radius = ref.radius
vars.point = ref.point
end
if not (vars.ignoreColor and vars.ignoreColor == true) and not vars.fillColor then
vars.fillColor = ref.color
end
--log:warn(vars)
return mist.marker.add(vars)
end
end
function mist.marker.drawShape(name, v)
if mist.DBs.drawingByName[name] then
local d = v or {}
local o = mist.utils.deepCopy(mist.DBs.drawingByName[name])
--mist.marker.add({point = {x = o.mapX, z = o.mapY}, text = name})
--log:warn(o)
d.points = o.points or {}
if o.primitiveType == "Polygon" then
d.mType = 7
if o.polygonMode == "rect" then
d.mType = 6
elseif o.polygonMode == "circle" then
d.mType = 2
d.points = {x = o.mapX, y = o.mapY}
d.radius = o.radius
end
elseif o.primitiveType == "TextBox" then
d.mType = 5
d.points = {x = o.mapX, y = o.mapY}
d.text = o.text or d.text
d.fontSize = d.fontSize or o.fontSize
end
-- NOTE TO SELF. FIGURE OUT WHICH SHAPES NEED TO BE OFFSET. OVAL YES.
if o.fillColorString and not d.fillColor then
d.fillColor = mist.utils.hexToRGB(o.fillColorString)
end
if o.colorString then
d.color = mist.utils.hexToRGB(o.colorString)
end
if o.thickness == 0 then
d.lineType = 0
elseif o.style == 'solid' then
d.lineType = 1
elseif o.style == 'dot' then
d.lineType = 2
elseif o.style == 'dash' then
d.lineType = 3
else
d.lineType = 1
end
if o.primitiveType == "Line" and #d.points >= 2 then
d.mType = 1
local rtn = {}
for i = 1, #d.points -1 do
local var = mist.utils.deepCopy(d)
var.points = {}
var.points[1] = d.points[i]
var.points[2] = d.points[i+1]
table.insert(rtn, mist.marker.add(var))
end
return rtn
else
if d.mType then
--log:warn(d)
return mist.marker.add(d)
end
end
end
end
--[[
function mist.marker.circle(v)
end
]]
end
--- Time conversion functions.
-- @section mist.time
do -- mist.time scope
mist.time = {}
-- returns a string for specified military time
-- theTime is optional
-- if present current time in mil time is returned
-- if number or table the time is converted into mil tim
function mist.time.convertToSec(timeTable)
local timeInSec = 0
if timeTable and type(timeTable) == 'number' then
timeInSec = timeTable
elseif timeTable and type(timeTable) == 'table' and (timeTable.d or timeTable.h or timeTable.m or timeTable.s) then
if timeTable.d and type(timeTable.d) == 'number' then
timeInSec = timeInSec + (timeTable.d*86400)
end
if timeTable.h and type(timeTable.h) == 'number' then
timeInSec = timeInSec + (timeTable.h*3600)
end
if timeTable.m and type(timeTable.m) == 'number' then
timeInSec = timeInSec + (timeTable.m*60)
end
if timeTable.s and type(timeTable.s) == 'number' then
timeInSec = timeInSec + timeTable.s
end
end
return timeInSec
end
function mist.time.getDHMS(timeInSec)
if timeInSec and type(timeInSec) == 'number' then
local tbl = {d = 0, h = 0, m = 0, s = 0}
if timeInSec > 86400 then
while timeInSec > 86400 do
tbl.d = tbl.d + 1
timeInSec = timeInSec - 86400
end
end
if timeInSec > 3600 then
while timeInSec > 3600 do
tbl.h = tbl.h + 1
timeInSec = timeInSec - 3600
end
end
if timeInSec > 60 then
while timeInSec > 60 do
tbl.m = tbl.m + 1
timeInSec = timeInSec - 60
end
end
tbl.s = timeInSec
return tbl
else
log:error("Didn't recieve number")
return
end
end
function mist.getMilString(theTime)
local timeInSec = 0
if theTime then
timeInSec = mist.time.convertToSec(theTime)
else
timeInSec = mist.utils.round(timer.getAbsTime(), 0)
end
local DHMS = mist.time.getDHMS(timeInSec)
return tostring(string.format('%02d', DHMS.h) .. string.format('%02d',DHMS.m))
end
function mist.getClockString(theTime, hour)
local timeInSec = 0
if theTime then
timeInSec = mist.time.convertToSec(theTime)
else
timeInSec = mist.utils.round(timer.getAbsTime(), 0)
end
local DHMS = mist.time.getDHMS(timeInSec)
if hour then
if DHMS.h > 12 then
DHMS.h = DHMS.h - 12
return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s) .. ' PM')
else
return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s) .. ' AM')
end
else
return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s))
end
end
-- returns the date in string format
-- both variables optional
-- first val returns with the month as a string
-- 2nd val defins if it should be written the American way or the wrong way.
function mist.time.getDate(convert)
local cal = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} --
local date = {}
if not env.mission.date then -- Not likely to happen. Resaving mission auto updates this to remove it.
date.d = 0
date.m = 6
date.y = 2011
else
date.d = env.mission.date.Day
date.m = env.mission.date.Month
date.y = env.mission.date.Year
end
local start = 86400
local timeInSec = mist.utils.round(timer.getAbsTime())
if convert and type(convert) == 'number' then
timeInSec = convert
end
if timeInSec > 86400 then
while start < timeInSec do
if date.d >= cal[date.m] then
if date.m == 2 and date.d == 28 then -- HOLY COW we can edit years now. Gotta re-add this!
if date.y % 4 == 0 and date.y % 100 == 0 and date.y % 400 ~= 0 or date.y % 4 > 0 then
date.m = date.m + 1
date.d = 0
end
--date.d = 29
else
date.m = date.m + 1
date.d = 0
end
end
if date.m == 13 then
date.m = 1
date.y = date.y + 1
end
date.d = date.d + 1
start = start + 86400
end
end
return date
end
function mist.time.relativeToStart(time)
if type(time) == 'number' then
return time - timer.getTime0()
end
end
function mist.getDateString(rtnType, murica, oTime) -- returns date based on time
local word = {'January', 'Feburary', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' } -- 'etc
local curTime = 0
if oTime then
curTime = oTime
else
curTime = mist.utils.round(timer.getAbsTime())
end
local tbl = mist.time.getDate(curTime)
if rtnType then
if murica then
return tostring(word[tbl.m] .. ' ' .. tbl.d .. ' ' .. tbl.y)
else
return tostring(tbl.d .. ' ' .. word[tbl.m] .. ' ' .. tbl.y)
end
else
if murica then
return tostring(tbl.m .. '.' .. tbl.d .. '.' .. tbl.y)
else
return tostring(tbl.d .. '.' .. tbl.m .. '.' .. tbl.y)
end
end
end
--WIP
function mist.time.milToGame(milString, rtnType) --converts a military time. By default returns the abosolute time that event would occur. With optional value it returns how many seconds from time of call till that time.
local curTime = mist.utils.round(timer.getAbsTime())
local milTimeInSec = 0
if milString and type(milString) == 'string' and string.len(milString) >= 4 then
local hr = tonumber(string.sub(milString, 1, 2))
local mi = tonumber(string.sub(milString, 3))
milTimeInSec = milTimeInSec + (mi*60) + (hr*3600)
elseif milString and type(milString) == 'table' and (milString.d or milString.h or milString.m or milString.s) then
milTimeInSec = mist.time.convertToSec(milString)
end
local startTime = timer.getTime0()
local daysOffset = 0
if startTime > 86400 then
daysOffset = mist.utils.round(startTime/86400)
if daysOffset > 0 then
milTimeInSec = milTimeInSec *daysOffset
end
end
if curTime > milTimeInSec then
milTimeInSec = milTimeInSec + 86400
end
if rtnType then
milTimeInSec = milTimeInSec - startTime
end
return milTimeInSec
end
end
--- Group task functions.
-- @section tasks
do -- group tasks scope
mist.ground = {}
mist.fixedWing = {}
mist.heli = {}
mist.air = {}
mist.air.fixedWing = {}
mist.air.heli = {}
mist.ship = {}
--- Tasks group to follow a route.
-- This sets the mission task for the given group.
-- Any wrapped actions inside the path (like enroute
-- tasks) will be executed.
-- @tparam Group group group to task.
-- @tparam table path containing
-- points defining a route.
function mist.goRoute(group, path)
local misTask = {
id = 'Mission',
params = {
route = {
points = mist.utils.deepCopy(path),
},
},
}
if type(group) == 'string' then
group = Group.getByName(group)
end
if group then
local groupCon = group:getController()
if groupCon then
--log:warn(misTask)
groupCon:setTask(misTask)
return true
end
end
return false
end
-- same as getGroupPoints but returns speed and formation type along with vec2 of point}
function mist.getGroupRoute(groupIdent, task)
-- refactor to search by groupId and allow groupId and groupName as inputs
local gpId = groupIdent
if mist.DBs.MEgroupsByName[groupIdent] then
gpId = mist.DBs.MEgroupsByName[groupIdent].groupId
else
log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent)
end
for coa_name, coa_data in pairs(env.mission.coalition) do
if type(coa_data) == 'table' then
if coa_data.country then --there is a country table
for cntry_id, cntry_data in pairs(coa_data.country) do
for obj_cat_name, obj_cat_data in pairs(cntry_data) do
if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points
if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group!
for group_num, group_data in pairs(obj_cat_data.group) do
if group_data and group_data.groupId == gpId then -- this is the group we are looking for
if group_data.route and group_data.route.points and #group_data.route.points > 0 then
local points = {}
for point_num, point in pairs(group_data.route.points) do
local routeData = {}
if env.mission.version > 7 and env.mission.version < 19 then
routeData.name = env.getValueDictByKey(point.name)
else
routeData.name = point.name
end
if not point.point then
routeData.x = point.x
routeData.y = point.y
else
routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation.
end
routeData.form = point.action
routeData.speed = point.speed
routeData.alt = point.alt
routeData.alt_type = point.alt_type
routeData.airdromeId = point.airdromeId
routeData.helipadId = point.helipadId
routeData.type = point.type
routeData.action = point.action
if task then
routeData.task = point.task
end
points[point_num] = routeData
end
return points
end
log:error('Group route not defined in mission editor for groupId: $1', gpId)
return
end --if group_data and group_data.name and group_data.name == 'groupname'
end --for group_num, group_data in pairs(obj_cat_data.group) do
end --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then
end --if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then
end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do
end --for cntry_id, cntry_data in pairs(coa_data.country) do
end --if coa_data.country then --there is a country table
end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then
end --for coa_name, coa_data in pairs(mission.coalition) do
end
-- function mist.ground.buildPath() end -- ????
function mist.ground.patrolRoute(vars)
--log:info('patrol')
local tempRoute = {}
local useRoute = {}
local gpData = vars.gpData
if type(gpData) == 'string' then
gpData = Group.getByName(gpData)
end
local useGroupRoute
if not vars.useGroupRoute then
useGroupRoute = vars.gpData
else
useGroupRoute = vars.useGroupRoute
end
local routeProvided = false
if not vars.route then
if useGroupRoute then
tempRoute = mist.getGroupRoute(useGroupRoute)
end
else
useRoute = vars.route
local posStart = mist.getLeadPos(gpData)
useRoute[1] = mist.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed)
routeProvided = true
end
local overRideSpeed = vars.speed or 'default'
local pType = vars.pType
local offRoadForm = vars.offRoadForm or 'default'
local onRoadForm = vars.onRoadForm or 'default'
if routeProvided == false and #tempRoute > 0 then
local posStart = mist.getLeadPos(gpData)
useRoute[#useRoute + 1] = mist.ground.buildWP(posStart, offRoadForm, overRideSpeed)
for i = 1, #tempRoute do
local tempForm = tempRoute[i].action
local tempSpeed = tempRoute[i].speed
if offRoadForm == 'default' then
tempForm = tempRoute[i].action
end
if onRoadForm == 'default' then
onRoadForm = 'On Road'
end
if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then
tempForm = onRoadForm
else
tempForm = offRoadForm
end
if type(overRideSpeed) == 'number' then
tempSpeed = overRideSpeed
end
useRoute[#useRoute + 1] = mist.ground.buildWP(tempRoute[i], tempForm, tempSpeed)
end
if pType and string.lower(pType) == 'doubleback' then
local curRoute = mist.utils.deepCopy(useRoute)
for i = #curRoute, 2, -1 do
useRoute[#useRoute + 1] = mist.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed)
end
end
useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP
end
local cTask3 = {}
local newPatrol = {}
newPatrol.route = useRoute
newPatrol.gpData = gpData:getName()
cTask3[#cTask3 + 1] = 'mist.ground.patrolRoute('
cTask3[#cTask3 + 1] = mist.utils.oneLineSerialize(newPatrol)
cTask3[#cTask3 + 1] = ')'
cTask3 = table.concat(cTask3)
local tempTask = {
id = 'WrappedAction',
params = {
action = {
id = 'Script',
params = {
command = cTask3,
},
},
},
}
useRoute[#useRoute].task = tempTask
log:info(useRoute)
mist.goRoute(gpData, useRoute)
return
end
function mist.ground.patrol(gpData, pType, form, speed)
local vars = {}
if type(gpData) == 'table' and gpData:getName() then
gpData = gpData:getName()
end
vars.useGroupRoute = gpData
vars.gpData = gpData
vars.pType = pType
vars.offRoadForm = form
vars.speed = speed
mist.ground.patrolRoute(vars)
return
end
-- No longer accepts path
function mist.ground.buildWP(point, overRideForm, overRideSpeed)
local wp = {}
wp.x = point.x
if point.z then
wp.y = point.z
else
wp.y = point.y
end
local form, speed
if point.speed and not overRideSpeed then
wp.speed = point.speed
elseif type(overRideSpeed) == 'number' then
wp.speed = overRideSpeed
else
wp.speed = mist.utils.kmphToMps(20)
end
if point.form and not overRideForm then
form = point.form
else
form = overRideForm
end
if not form then
wp.action = 'Cone'
else
form = string.lower(form)
if form == 'off_road' or form == 'off road' then
wp.action = 'Off Road'
elseif form == 'on_road' or form == 'on road' then
wp.action = 'On Road'
elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then
wp.action = 'Rank'
elseif form == 'cone' then
wp.action = 'Cone'
elseif form == 'diamond' then
wp.action = 'Diamond'
elseif form == 'vee' then
wp.action = 'Vee'
elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then
wp.action = 'EchelonL'
elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then
wp.action = 'EchelonR'
else
wp.action = 'Cone' -- if nothing matched
end
end
wp.type = 'Turning Point'
return wp
end
function mist.fixedWing.buildWP(point, WPtype, speed, alt, altType)
local wp = {}
wp.x = point.x
if point.z then
wp.y = point.z
else
wp.y = point.y
end
if alt and type(alt) == 'number' then
wp.alt = alt
else
wp.alt = 2000
end
if altType then
altType = string.lower(altType)
if altType == 'radio' or altType == 'agl' then
wp.alt_type = 'RADIO'
elseif altType == 'baro' or altType == 'asl' then
wp.alt_type = 'BARO'
end
else
wp.alt_type = 'RADIO'
end
if point.speed then
speed = point.speed
end
if point.type then
WPtype = point.type
end
if not speed then
wp.speed = mist.utils.kmphToMps(500)
else
wp.speed = speed
end
if not WPtype then
wp.action = 'Turning Point'
else
WPtype = string.lower(WPtype)
if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then
wp.action = 'Fly Over Point'
elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then
wp.action = 'Turning Point'
else
wp.action = 'Turning Point'
end
end
wp.type = 'Turning Point'
return wp
end
function mist.heli.buildWP(point, WPtype, speed, alt, altType)
local wp = {}
wp.x = point.x
if point.z then
wp.y = point.z
else
wp.y = point.y
end
if alt and type(alt) == 'number' then
wp.alt = alt
else
wp.alt = 500
end
if altType then
altType = string.lower(altType)
if altType == 'radio' or altType == 'agl' then
wp.alt_type = 'RADIO'
elseif altType == 'baro' or altType == 'asl' then
wp.alt_type = 'BARO'
end
else
wp.alt_type = 'RADIO'
end
if point.speed then
speed = point.speed
end
if point.type then
WPtype = point.type
end
if not speed then
wp.speed = mist.utils.kmphToMps(200)
else
wp.speed = speed
end
if not WPtype then
wp.action = 'Turning Point'
else
WPtype = string.lower(WPtype)
if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then
wp.action = 'Fly Over Point'
elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then
wp.action = 'Turning Point'
else
wp.action = 'Turning Point'
end
end
wp.type = 'Turning Point'
return wp
end
-- need to return a Vec3 or Vec2?
function mist.getRandPointInCircle(p, r, innerRadius, maxA, minA)
local point = mist.utils.makeVec3(p)
local theta = 2*math.pi*math.random()
local radius = r or 1000
local minR = innerRadius or 0
if maxA and not minA then
theta = math.rad(math.random(0, maxA - math.random()))
elseif maxA and minA then
if minA < maxA then
theta = math.rad(math.random(minA, maxA) - math.random())
else
theta = math.rad(math.random(maxA, minA) - math.random())
end
end
local rad = math.random() + math.random()
if rad > 1 then
rad = 2 - rad
end
local radMult
if minR and minR <= radius then
--radMult = (radius - innerRadius)*rad + innerRadius
radMult = radius * math.sqrt((minR^2 + (radius^2 - minR^2) * math.random()) / radius^2)
else
radMult = radius*rad
end
local rndCoord
if radius > 0 then
rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z}
else
rndCoord = {x = point.x, y = point.z}
end
return rndCoord
end
function mist.getRandomPointInZone(zoneName, innerRadius, maxA, minA)
if type(zoneName) == 'string' then
local zone = mist.DBs.zonesByName[zoneName]
if zone.type and zone.type == 2 then
return mist.getRandomPointInPoly(zone.verticies)
else
return mist.getRandPointInCircle(zone.point, zone.radius, innerRadius, maxA, minA)
end
end
return false
end
function mist.getRandomPointInPoly(zone)
--env.info('Zone Size: '.. #zone)
local avg = mist.getAvgPoint(zone)
--log:warn(avg)
local radius = 0
local minR = math.huge
local newCoord = {}
for i = 1, #zone do
if mist.utils.get2DDist(avg, zone[i]) > radius then
radius = mist.utils.get2DDist(avg, zone[i])
end
if mist.utils.get2DDist(avg, zone[i]) < minR then
minR = mist.utils.get2DDist(avg, zone[i])
end
end
--log:warn('minR: $1', minR)
--log:warn('Radius: $1', radius)
local lSpawnPos = {}
for j = 1, 100 do
newCoord = mist.getRandPointInCircle(avg, radius)
if mist.pointInPolygon(newCoord, zone) then
break
end
if j == 100 then
newCoord = mist.getRandPointInCircle(avg, radius)
log:warn("Failed to find point in poly; Giving random point from center of the poly")
end
end
return newCoord
end
function mist.getWindBearingAndVel(p)
local point = mist.utils.makeVec3(p)
local gLevel = land.getHeight({x = point.x, y = point.z})
if point.y <= gLevel then
point.y = gLevel + 10
end
local t = atmosphere.getWind(point)
local bearing = math.atan2(t.z, t.x)
local vel = math.sqrt(t.x^2 + t.z^2)
return bearing, vel
end
function mist.groupToRandomPoint(vars)
local group = vars.group --Required
local point = vars.point --required
local radius = vars.radius or 0
local innerRadius = vars.innerRadius
local form = vars.form or 'Cone'
local heading = vars.heading or math.random()*2*math.pi
local headingDegrees = vars.headingDegrees
local speed = vars.speed or mist.utils.kmphToMps(20)
local useRoads
if not vars.disableRoads then
useRoads = true
else
useRoads = false
end
local path = {}
if headingDegrees then
heading = headingDegrees*math.pi/180
end
if heading >= 2*math.pi then
heading = heading - 2*math.pi
end
local rndCoord = mist.getRandPointInCircle(point, radius, innerRadius)
local offset = {}
local posStart = mist.getLeadPos(group)
if posStart then
offset.x = mist.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3)
offset.z = mist.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3)
path[#path + 1] = mist.ground.buildWP(posStart, form, speed)
if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then
path[#path + 1] = mist.ground.buildWP({x = posStart.x + 11, z = posStart.z + 11}, 'off_road', speed)
path[#path + 1] = mist.ground.buildWP(posStart, 'on_road', speed)
path[#path + 1] = mist.ground.buildWP(offset, 'on_road', speed)
else
path[#path + 1] = mist.ground.buildWP({x = posStart.x + 25, z = posStart.z + 25}, form, speed)
end
end
path[#path + 1] = mist.ground.buildWP(offset, form, speed)
path[#path + 1] = mist.ground.buildWP(rndCoord, form, speed)
mist.goRoute(group, path)
return
end
function mist.groupRandomDistSelf(gpData, dist, form, heading, speed, disableRoads)
local pos = mist.getLeadPos(gpData)
local fakeZone = {}
fakeZone.radius = dist or math.random(300, 1000)
fakeZone.point = {x = pos.x, y = pos.y, z = pos.z}
mist.groupToRandomZone(gpData, fakeZone, form, heading, speed, disableRoads)
return
end
function mist.groupToRandomZone(gpData, zone, form, heading, speed, disableRoads)
if type(gpData) == 'string' then
gpData = Group.getByName(gpData)
end
if type(zone) == 'string' then
zone = mist.DBs.zonesByName[zone]
elseif type(zone) == 'table' and not zone.radius then
zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]]
end
if speed then
speed = mist.utils.kmphToMps(speed)
end
local vars = {}
vars.group = gpData
vars.radius = zone.radius
vars.form = form
vars.headingDegrees = heading
vars.speed = speed
vars.point = mist.utils.zoneToVec3(zone)
vars.disableRoads = disableRoads
mist.groupToRandomPoint(vars)
return
end
function mist.isTerrainValid(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types
if coord.z then
coord.y = coord.z
end
local typeConverted = {}
if type(terrainTypes) == 'string' then -- if its a string it does this check
for constId, constData in pairs(land.SurfaceType) do
if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then
table.insert(typeConverted, constId)
end
end
elseif type(terrainTypes) == 'table' then -- if its a table it does this check
for typeId, typeData in pairs(terrainTypes) do
for constId, constData in pairs(land.SurfaceType) do
if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeData) then
table.insert(typeConverted, constId)
end
end
end
end
for validIndex, validData in pairs(typeConverted) do
if land.getSurfaceType(coord) == land.SurfaceType[validData] then
log:info('Surface is : $1', validData)
return true
end
end
return false
end
function mist.terrainHeightDiff(coord, searchSize)
local samples = {}
local searchRadius = 5
if searchSize then
searchRadius = searchSize
end
if type(coord) == 'string' then
coord = mist.utils.zoneToVec3(coord)
end
coord = mist.utils.makeVec2(coord)
samples[#samples + 1] = land.getHeight(coord)
for i = 0, 360, 30 do
samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*searchRadius)), y = (coord.y + (math.cos(math.rad(i))*searchRadius))})
if searchRadius >= 20 then -- if search radius is sorta large, take a sample halfway between center and outer edge
samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*(searchRadius/2))), y = (coord.y + (math.cos(math.rad(i))*(searchRadius/2)))})
end
end
local tMax, tMin = 0, 1000000
for index, height in pairs(samples) do
if height > tMax then
tMax = height
end
if height < tMin then
tMin = height
end
end
return mist.utils.round(tMax - tMin, 2)
end
function mist.groupToPoint(gpData, point, form, heading, speed, useRoads)
if type(point) == 'string' then
point = mist.DBs.zonesByName[point]
end
if speed then
speed = mist.utils.kmphToMps(speed)
end
local vars = {}
vars.group = gpData
vars.form = form
vars.headingDegrees = heading
vars.speed = speed
vars.disableRoads = useRoads
vars.point = mist.utils.zoneToVec3(point)
mist.groupToRandomPoint(vars)
return
end
function mist.getLeadPos(group)
local gObj
if type(group) == 'string' then -- group name
gObj = Group.getByName(group)
elseif type(group) == "table" then
gObj = group
end
if gObj then
local units = gObj:getUnits()
local leader = units[1]
if leader then
if Unit.isExist(leader) then
return leader:getPoint()
elseif #units > 1 then
for i = 2, #units do
if Unit.isExist(units[i]) then
return units[i]:getPoint()
end
end
end
end
end
log:error("Group passed to mist.getLeadPos might be dead: $1", group)
end
function mist.groupIsDead(groupName) -- copy more or less from on station
local gp = Group.getByName(groupName)
if gp then
if #gp:getUnits() > 0 and gp:isExist() == true then
return false
end
end
return true
end
function mist.pointInZone(point, zone)
local ref = mist.utils.deepCopy(zone)
if type(zone) == 'string' then
ref = mist.DBs.zonesByName[zone]
end
if ref.verticies then
return mist.pointInPolygon(point, ref.verticies)
else
return mist.utils.get2DDist(point, ref.point) < ref.radius
end
end
end
--- Database tables.
-- @section mist.DBs
--- Mission data
-- @table mist.DBs.missionData
-- @field startTime mission start time
-- @field theatre mission theatre/map e.g. Caucasus
-- @field version mission version
-- @field files mission resources
--- Tables used as parameters.
-- @section varTables
--- mist.flagFunc.units_in_polygon parameter table.
-- @table unitsInPolygonVars
-- @tfield table unit name table @{UnitNameTable}.
-- @tfield table zone table defining a polygon.
-- @tfield number|string flag flag to set to true.
-- @tfield[opt] number|string stopflag if set to true the function
-- will stop evaluating.
-- @tfield[opt] number maxalt maximum altitude (MSL) for the
-- polygon.
-- @tfield[opt] number req_num minimum number of units that have
-- to be in the polygon.
-- @tfield[opt] number interval sets the interval for
-- checking if units are inside of the polygon in seconds. Default: 1.
-- @tfield[opt] boolean toggle switch the flag to false if required
-- conditions are not met. Default: false.
-- @tfield[opt] table unitTableDef
--- Logger class.
-- @type mist.Logger
do -- mist.Logger scope
mist.Logger = {}
--- parses text and substitutes keywords with values from given array.
-- @param text string containing keywords to substitute with values
-- or a variable.
-- @param ... variables to use for substitution in string.
-- @treturn string new string with keywords substituted or
-- value of variable as string.
local function formatText(text, ...)
if type(text) ~= 'string' then
if type(text) == 'table' then
text = mist.utils.oneLineSerialize(text)
else
text = tostring(text)
end
else
for index,value in ipairs(arg) do
-- TODO: check for getmetatabel(value).__tostring
if type(value) == 'table' then
value = mist.utils.oneLineSerialize(value)
else
value = tostring(value)
end
text = text:gsub('$' .. index, value)
end
end
local fName = nil
local cLine = nil
if debug then
local dInfo = debug.getinfo(3)
fName = dInfo.name
cLine = dInfo.currentline
-- local fsrc = dinfo.short_src
--local fLine = dInfo.linedefined
end
if fName and cLine then
return fName .. '|' .. cLine .. ': ' .. text
elseif cLine then
return cLine .. ': ' .. text
else
return ' ' .. text
end
end
local function splitText(text)
local tbl = {}
while text:len() > 4000 do
local sub = text:sub(1, 4000)
text = text:sub(4001)
table.insert(tbl, sub)
end
table.insert(tbl, text)
return tbl
end
--- Creates a new logger.
-- Each logger has it's own tag and log level.
-- @tparam string tag tag which appears at the start of
-- every log line produced by this logger.
-- @tparam[opt] number|string level the log level defines which messages
-- will be logged and which will be omitted. Log level 3 beeing the most verbose
-- and 0 disabling all output. This can also be a string. Allowed strings are:
-- "none" (0), "error" (1), "warning" (2) and "info" (3).
-- @usage myLogger = mist.Logger:new("MyScript")
-- @usage myLogger = mist.Logger:new("MyScript", 2)
-- @usage myLogger = mist.Logger:new("MyScript", "info")
-- @treturn mist.Logger
function mist.Logger:new(tag, level)
local l = {tag = tag}
setmetatable(l, self)
self.__index = self
l:setLevel(level)
return l
end
--- Sets the level of verbosity for this logger.
-- @tparam[opt] number|string level the log level defines which messages
-- will be logged and which will be omitted. Log level 3 beeing the most verbose
-- and 0 disabling all output. This can also[ be a string. Allowed strings are:
-- "none" (0), "error" (1), "warning" (2) and "info" (3).
-- @usage myLogger:setLevel("info")
-- @usage -- log everything
--myLogger:setLevel(3)
function mist.Logger:setLevel(level)
self.level = 2
if level then
if type(level) == 'string' then
level = string.lower(level)
if level == 'none' or level == 'off' then
self.level = 0
elseif level == 'error' then
self.level = 1
elseif level == 'warning' or level == 'warn' then
self.level = 2
elseif level == 'info' then
self.level = 3
end
elseif type(level) == 'number' then
self.level = level
end
end
end
--- Logs error and shows alert window.
-- This logs an error to the dcs.log and shows a popup window,
-- pausing the simulation. This works always even if logging is
-- disabled by setting a log level of "none" or 0.
-- @tparam string text the text with keywords to substitute.
-- @param ... variables to be used for substitution.
-- @usage myLogger:alert("Shit just hit the fan! WEEEE!!!11")
function mist.Logger:alert(text, ...)
text = formatText(text, unpack(arg))
if text:len() > 4000 then
local texts = splitText(text)
for i = 1, #texts do
if i == 1 then
env.error(self.tag .. '|' .. texts[i], true)
else
env.error(texts[i])
end
end
else
env.error(self.tag .. '|' .. text, true)
end
end
--- Logs a message, disregarding the log level.
-- @tparam string text the text with keywords to substitute.
-- @param ... variables to be used for substitution.
-- @usage myLogger:msg("Always logged!")
function mist.Logger:msg(text, ...)
text = formatText(text, unpack(arg))
if text:len() > 4000 then
local texts = splitText(text)
for i = 1, #texts do
if i == 1 then
env.info(self.tag .. '|' .. texts[i])
else
env.info(texts[i])
end
end
else
env.info(self.tag .. '|' .. text)
end
end
--- Logs an error.
-- logs a message prefixed with this loggers tag to dcs.log as
-- long as at least the "error" log level (1) is set.
-- @tparam string text the text with keywords to substitute.
-- @param ... variables to be used for substitution.
-- @usage myLogger:error("Just an error!")
-- @usage myLogger:error("Foo is $1 instead of $2", foo, "bar")
function mist.Logger:error(text, ...)
if self.level >= 1 then
text = formatText(text, unpack(arg))
if text:len() > 4000 then
local texts = splitText(text)
for i = 1, #texts do
if i == 1 then
env.error(self.tag .. '|' .. texts[i])
else
env.error(texts[i])
end
end
else
env.error(self.tag .. '|' .. text, mistSettings.errorPopup)
end
end
end
--- Logs a message, disregarding the log level and displays a message out text box.
-- @tparam string text the text with keywords to substitute.
-- @param ... variables to be used for substitution.
-- @usage myLogger:msg("Always logged!")
function mist.Logger:echo(text, ...)
text = formatText(text, unpack(arg))
if text:len() > 4000 then
local texts = splitText(text)
for i = 1, #texts do
if i == 1 then
env.info(self.tag .. '|' .. texts[i])
else
env.info(texts[i])
end
end
else
env.info(self.tag .. '|' .. text)
end
trigger.action.outText(text, 30)
end
--- Logs a warning.
-- logs a message prefixed with this loggers tag to dcs.log as
-- long as at least the "warning" log level (2) is set.
-- @tparam string text the text with keywords to substitute.
-- @param ... variables to be used for substitution.
-- @usage myLogger:warn("Mother warned you! Those $1 from the interwebs are $2", {"geeks", 1337})
function mist.Logger:warn(text, ...)
if self.level >= 2 then
text = formatText(text, unpack(arg))
if text:len() > 4000 then
local texts = splitText(text)
for i = 1, #texts do
if i == 1 then
env.warning(self.tag .. '|' .. texts[i])
else
env.warning(texts[i])
end
end
else
env.warning(self.tag .. '|' .. text, mistSettings.warnPopup)
end
end
end
--- Logs a info.
-- logs a message prefixed with this loggers tag to dcs.log as
-- long as the highest log level (3) "info" is set.
-- @tparam string text the text with keywords to substitute.
-- @param ... variables to be used for substitution.
-- @see warn
function mist.Logger:info(text, ...)
if self.level >= 3 then
text = formatText(text, unpack(arg))
if text:len() > 4000 then
local texts = splitText(text)
for i = 1, #texts do
if i == 1 then
env.info(self.tag .. '|' .. texts[i])
else
env.info(texts[i])
end
end
else
env.info(self.tag .. '|' .. text, mistSettings.infoPopup)
end
end
end
end
-- initialize mist
mist.init()
env.info(('Mist version ' .. mist.majorVersion .. '.' .. mist.minorVersion .. '.' .. mist.build .. ' loaded.'))
-- vim: noet:ts=2:sw=2