--[[-- 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: ##Github: Development Official Releases @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]" - subtract this unit if its in the table "[g]" - add this group to the table "[-g]" - subtract this group from the table "[c]" - add this country's units "[-c]" - 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]" - add all of this country's helicopters "[-c][helicopter]" - subtract all of this country's helicopters "[c][plane]" - add all of this country's planes "[-c][plane]" - subtract all of this country's planes "[c][ship]" - add all of this country's ships "[-c][ship]" - subtract all of this country's ships "[c][vehicle]" - add all of this country's vehicles "[-c][vehicle]" - 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