--- **Utilities** - Derived utilities taken from the MIST framework, added helpers from the MOOSE community. -- -- ### Authors: -- -- * Grimes : Design & Programming of the MIST framework. -- -- ### Contributions: -- -- * FlightControl : Rework to OO framework. -- * And many more -- -- @module Utilities.Utils -- @image MOOSE.JPG --- -- @type SMOKECOLOR -- @field Green -- @field Red -- @field White -- @field Orange -- @field Blue SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR --- -- @type FLARECOLOR -- @field Green -- @field Red -- @field White -- @field Yellow FLARECOLOR = trigger.flareColor -- #FLARECOLOR --- Big smoke preset enum. -- @type BIGSMOKEPRESET BIGSMOKEPRESET = { SmallSmokeAndFire=1, MediumSmokeAndFire=2, LargeSmokeAndFire=3, HugeSmokeAndFire=4, SmallSmoke=5, MediumSmoke=6, LargeSmoke=7, HugeSmoke=8, } --- DCS map as returned by `env.mission.theatre`. -- @type DCSMAP -- @field #string Caucasus Caucasus map. -- @field #string Normandy Normandy map. -- @field #string NTTR Nevada Test and Training Range map. -- @field #string PersianGulf Persian Gulf map. -- @field #string TheChannel The Channel map. -- @field #string Syria Syria map. -- @field #string MarianaIslands Mariana Islands map. -- @field #string Falklands South Atlantic map. -- @field #string Sinai Sinai map. DCSMAP = { Caucasus="Caucasus", NTTR="Nevada", Normandy="Normandy", PersianGulf="PersianGulf", TheChannel="TheChannel", Syria="Syria", MarianaIslands="MarianaIslands", Falklands="Falklands", Sinai="SinaiMap" } --- See [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns) -- @type CALLSIGN CALLSIGN={ -- Aircraft Aircraft={ Enfield=1, Springfield=2, Uzi=3, Colt=4, Dodge=5, Ford=6, Chevy=7, Pontiac=8, -- A-10A or A-10C Hawg=9, Boar=10, Pig=11, Tusk=12, }, -- AWACS AWACS={ Overlord=1, Magic=2, Wizard=3, Focus=4, Darkstar=5, }, -- Tanker Tanker={ Texaco=1, Arco=2, Shell=3, Navy_One=4, Mauler=5, Bloodhound=6, }, -- JTAC 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, }, -- FARP FARP={ London=1, Dallas=2, Paris=3, Moscow=4, Berlin=5, Rome=6, Madrid=7, Warsaw=8, Dublin=9, Perth=10, }, 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, }, 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, }, F15E={ Dude=9, Thud=10, Gunny=11, Trek=12, Sniper=13, Sled=14, Best=15, Jazz=16, Rage=17, Tahoe=18, }, B1B={ Bone=9, Dark=10, Vader=11 }, B52={ Buff=9, Dump=10, Kenworth=11, }, TransportAircraft={ Heavy=9, Trash=10, Cargo=11, Ascot=12, }, } --#CALLSIGN --- Utilities static class. -- @type UTILS -- @field #number _MarkID Marker index counter. Running number when marker is added. UTILS = { _MarkID = 1 } --- Function to infer instance of an object -- -- ### Examples: -- -- * UTILS.IsInstanceOf( 'some text', 'string' ) will return true -- * UTILS.IsInstanceOf( some_function, 'function' ) will return true -- * UTILS.IsInstanceOf( 10, 'number' ) will return true -- * UTILS.IsInstanceOf( false, 'boolean' ) will return true -- * UTILS.IsInstanceOf( nil, 'nil' ) will return true -- -- * UTILS.IsInstanceOf( ZONE:New( 'some zone', ZONE ) will return true -- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'ZONE' ) will return true -- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'zone' ) will return true -- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'BASE' ) will return true -- -- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'GROUP' ) will return false -- -- -- @param object is the object to be evaluated -- @param className is the name of the class to evaluate (can be either a string or a Moose class) -- @return #boolean UTILS.IsInstanceOf = function( object, className ) -- Is className NOT a string ? if type( className ) ~= 'string' then -- Is className a Moose class ? if type( className ) == 'table' and className.IsInstanceOf ~= nil then -- Get the name of the Moose class as a string className = className.ClassName -- className is neither a string nor a Moose class, throw an error else -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall local err_str = 'className parameter should be a string; parameter received: '..type( className ) return false -- error( err_str ) end end -- Is the object a Moose class instance ? if type( object ) == 'table' and object.IsInstanceOf ~= nil then -- Use the IsInstanceOf method of the BASE class return object:IsInstanceOf( className ) else -- If the object is not an instance of a Moose class, evaluate against lua basic data types local basicDataTypes = { 'string', 'number', 'function', 'boolean', 'nil', 'table' } for _, basicDataType in ipairs( basicDataTypes ) do if className == basicDataType then return type( object ) == basicDataType end end end -- Check failed return false end --- Deep copy a table. See http://lua-users.org/wiki/CopyTable -- @param #table object The input table. -- @return #table Copy of the input table. UTILS.DeepCopy = function(object) local lookup_table = {} -- Copy function. 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 local objectreturn = _copy(object) return objectreturn end --- Serialize a given table. -- @param #table tbl Input table. -- @return #string Table as a string. UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function lookup_table = {} local function _Serialize( tbl ) if type(tbl) == 'table' then --function only works for tables! if lookup_table[tbl] then return lookup_table[object] end local tbl_str = {} lookup_table[tbl] = tbl_str tbl_str[#tbl_str + 1] = '{' for ind,val in pairs(tbl) do -- serialize its fields local ind_str = {} if type(ind) == "number" then ind_str[#ind_str + 1] = '[' ind_str[#ind_str + 1] = tostring(ind) ind_str[#ind_str + 1] = ']=' else --must be a string ind_str[#ind_str + 1] = '[' ind_str[#ind_str + 1] = UTILS.BasicSerialize(ind) ind_str[#ind_str + 1] = ']=' end local val_str = {} if ((type(val) == 'number') or (type(val) == 'boolean')) then val_str[#val_str + 1] = tostring(val) val_str[#val_str + 1] = ',' tbl_str[#tbl_str + 1] = table.concat(ind_str) tbl_str[#tbl_str + 1] = table.concat(val_str) elseif type(val) == 'string' then val_str[#val_str + 1] = UTILS.BasicSerialize(val) val_str[#val_str + 1] = ',' tbl_str[#tbl_str + 1] = table.concat(ind_str) tbl_str[#tbl_str + 1] = table.concat(val_str) elseif type(val) == 'nil' then -- won't ever happen, right? val_str[#val_str + 1] = 'nil,' tbl_str[#tbl_str + 1] = table.concat(ind_str) tbl_str[#tbl_str + 1] = table.concat(val_str) elseif type(val) == 'table' then if ind == "__index" then -- tbl_str[#tbl_str + 1] = "__index" -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it else val_str[#val_str + 1] = _Serialize(val) val_str[#val_str + 1] = ',' --I think this is right, I just added it tbl_str[#tbl_str + 1] = table.concat(ind_str) tbl_str[#tbl_str + 1] = table.concat(val_str) end elseif type(val) == 'function' then tbl_str[#tbl_str + 1] = "f() " .. tostring(ind) tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it else env.info('unable to serialize value type ' .. UTILS.BasicSerialize(type(val)) .. ' at index ' .. tostring(ind)) env.info( debug.traceback() ) end end tbl_str[#tbl_str + 1] = '}' return table.concat(tbl_str) else return tostring(tbl) end end local objectreturn = _Serialize(tbl) return objectreturn end --- Serialize a table to a single line string. -- @param #table tbl table to serialize. -- @return #string string containing serialized table. function 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] = 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] = 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] = UTILS.TableShow(tbl,loc,indent,tableshow_tbls) --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 UTILS.BasicSerialize(tbl) end end --- Basic serialize (porting in Slmod's "safestring" basic serialize). -- @param #string s Table to serialize. UTILS.BasicSerialize = function(s) if s == nil then return "\"\"" else if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'userdata') ) then return tostring(s) elseif type(s) == "table" then return UTILS._OneLineSerialize(s) elseif type(s) == 'string' then s = string.format('(%s)', s) return s end end end --- Print a table to log in a nice format -- @param #table table The table to print -- @param #number indent Number of indents -- @return #string text Text created on the fly of the log output function UTILS.PrintTableToLog(table, indent) local text = "\n" if not table then env.warning("No table passed!") return nil end if not indent then indent = 0 end for k, v in pairs(table) do if string.find(k," ") then k='"'..k..'"'end if type(v) == "table" then env.info(string.rep(" ", indent) .. tostring(k) .. " = {") text = text ..string.rep(" ", indent) .. tostring(k) .. " = {\n" text = text .. tostring(UTILS.PrintTableToLog(v, indent + 1)).."\n" env.info(string.rep(" ", indent) .. "},") text = text .. string.rep(" ", indent) .. "},\n" else local value if tostring(v) == "true" or tostring(v) == "false" or tonumber(v) ~= nil then value=v else value = '"'..tostring(v)..'"' end env.info(string.rep(" ", indent) .. tostring(k) .. " = " .. tostring(value)..",\n") text = text .. string.rep(" ", indent) .. tostring(k) .. " = " .. tostring(value)..",\n" end end return text end --- Returns table in a easy readable string representation. -- @param tbl table to show -- @param loc -- @param indent -- @param tableshow_tbls -- @return Human readable string representation of given table. function UTILS.TableShow(tbl, loc, indent, tableshow_tbls) 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 -- serialize its fields 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] = 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] = 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 .. '[' .. UTILS.BasicSerialize(ind) .. ']' tbl_str[#tbl_str + 1] = tostring(val) .. ' ' tbl_str[#tbl_str + 1] = UTILS.TableShow(val, loc .. '[' .. 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 ' .. UTILS.BasicSerialize(type(val)) .. ' at index ' .. tostring(ind) end end tbl_str[#tbl_str + 1] = indent .. '}' return table.concat(tbl_str) 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 #string fname File name. function UTILS.Gdump(fname) if lfs and io then local fdir = lfs.writedir() .. [[Logs\]] .. fname local f = io.open(fdir, 'w') f:write(UTILS.TableShow(_G)) f:close() env.info(string.format('Wrote debug data to $1', fdir)) else env.error("WARNING: lfs and/or io not de-sanitized - cannot dump _G!") end end --- Executes the given string. -- borrowed from Slmod -- @param #string s string containing LUA code. -- @return #boolean `true` if successfully executed, `false` otherwise. function UTILS.DoString(s) local f, err = loadstring(s) if f then return true, f() else return false, err end end UTILS.ToDegree = function(angle) return angle*180/math.pi end UTILS.ToRadian = function(angle) return angle*math.pi/180 end UTILS.MetersToNM = function(meters) return meters/1852 end UTILS.KiloMetersToNM = function(kilometers) return kilometers/1852*1000 end UTILS.MetersToSM = function(meters) return meters/1609.34 end UTILS.KiloMetersToSM = function(kilometers) return kilometers/1609.34*1000 end UTILS.MetersToFeet = function(meters) return meters/0.3048 end UTILS.KiloMetersToFeet = function(kilometers) return kilometers/0.3048*1000 end UTILS.NMToMeters = function(NM) return NM*1852 end UTILS.NMToKiloMeters = function(NM) return NM*1852/1000 end UTILS.FeetToMeters = function(feet) return feet*0.3048 end UTILS.KnotsToKmph = function(knots) return knots * 1.852 end UTILS.KmphToKnots = function(knots) return knots / 1.852 end UTILS.KmphToMps = function( kmph ) return kmph / 3.6 end UTILS.MpsToKmph = function( mps ) return mps * 3.6 end UTILS.MiphToMps = function( miph ) return miph * 0.44704 end --- Convert meters per second to miles per hour. -- @param #number mps Speed in m/s. -- @return #number Speed in miles per hour. UTILS.MpsToMiph = function( mps ) return mps / 0.44704 end --- Convert meters per second to knots. -- @param #number mps Speed in m/s. -- @return #number Speed in knots. UTILS.MpsToKnots = function( mps ) return mps * 1.94384 --3600 / 1852 end --- Convert knots to meters per second. -- @param #number knots Speed in knots. -- @return #number Speed in m/s. UTILS.KnotsToMps = function( knots ) if type(knots) == "number" then return knots / 1.94384 --* 1852 / 3600 else return 0 end end --- Convert temperature from Celsius to Fahrenheit. -- @param #number Celcius Temperature in degrees Celsius. -- @return #number Temperature in degrees Fahrenheit. UTILS.CelsiusToFahrenheit = function( Celcius ) return Celcius * 9/5 + 32 end --- Convert pressure from hecto Pascal (hPa) to inches of mercury (inHg). -- @param #number hPa Pressure in hPa. -- @return #number Pressure in inHg. UTILS.hPa2inHg = function( hPa ) return hPa * 0.0295299830714 end --- Convert indicated airspeed (IAS) to true airspeed (TAS) for a given altitude above main sea level. -- The conversion is based on the approximation that TAS is ~2% higher than IAS with every 1000 ft altitude above sea level. -- @param #number ias Indicated air speed in any unit (m/s, km/h, knots, ...) -- @param #number altitude Altitude above main sea level in meters. -- @param #number oatcorr (Optional) Outside air temperature correction factor. Default 0.017. -- @return #number True airspeed in the same unit the IAS has been given. UTILS.IasToTas = function( ias, altitude, oatcorr ) oatcorr=oatcorr or 0.017 local tas=ias + (ias * oatcorr * UTILS.MetersToFeet(altitude) / 1000) return tas end --- Convert true airspeed (TAS) to indicated airspeed (IAS) for a given altitude above main sea level. -- The conversion is based on the approximation that TAS is ~2% higher than IAS with every 1000 ft altitude above sea level. -- @param #number tas True air speed in any unit (m/s, km/h, knots, ...) -- @param #number altitude Altitude above main sea level in meters. -- @param #number oatcorr (Optional) Outside air temperature correction factor. Default 0.017. -- @return #number Indicated airspeed in the same unit the TAS has been given. UTILS.TasToIas = function( tas, altitude, oatcorr ) oatcorr=oatcorr or 0.017 local ias=tas/(1+oatcorr*UTILS.MetersToFeet(altitude)/1000) return ias end --- Convert knots to altitude corrected KIAS, e.g. for tankers. -- @param #number knots Speed in knots. -- @param #number altitude Altitude in feet -- @return #number Corrected KIAS UTILS.KnotsToAltKIAS = function( knots, altitude ) return (knots * 0.018 * (altitude / 1000)) + knots end --- Convert pressure from hecto Pascal (hPa) to millimeters of mercury (mmHg). -- @param #number hPa Pressure in hPa. -- @return #number Pressure in mmHg. UTILS.hPa2mmHg = function( hPa ) return hPa * 0.7500615613030 end --- Convert kilo gramms (kg) to pounds (lbs). -- @param #number kg Mass in kg. -- @return #number Mass in lbs. UTILS.kg2lbs = function( kg ) return kg * 2.20462 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. ]] UTILS.tostringLL = function( 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 = UTILS.Round((oldLatMin - latMin)*60, acc) local oldLonMin = lonMin lonMin = math.floor(lonMin) local lonSec = 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 secFrmtStr = '%02d' if acc <= 0 then -- no decimal place. secFrmtStr = '%02d' else local width = 3 + acc -- 01.310 - that's a width of 6, for example. Acc is limited to 2 for DMS! secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' end -- 024° 23' 12"N or 024° 23' 12.03"N return string.format('%03d°', latDeg)..string.format('%02d', latMin)..'\''..string.format(secFrmtStr, latSec)..'"'..latHemi..' ' .. string.format('%03d°', lonDeg)..string.format('%02d', lonMin)..'\''..string.format(secFrmtStr, lonSec)..'"'..lonHemi else -- degrees, decimal minutes. latMin = UTILS.Round(latMin, acc) lonMin = 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 -- 024 23'N or 024 23.123'N return string.format('%03d°', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' .. string.format('%03d°', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi end end -- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. UTILS.tostringMGRS = function(MGRS, acc) --R2.1 if acc <= 0 then return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph else if acc > 5 then acc = 5 end -- Test if Easting/Northing have less than 4 digits. --MGRS.Easting=123 -- should be 00123 --MGRS.Northing=5432 -- should be 05432 -- Truncate rather than round MGRS grid! local Easting=tostring(MGRS.Easting) local Northing=tostring(MGRS.Northing) -- Count number of missing digits. Easting/Northing should have 5 digits. However, it is passed as a number. Therefore, any leading zeros would not be displayed by lua. local nE=5-string.len(Easting) local nN=5-string.len(Northing) -- Get leading zeros (if any). for i=1,nE do Easting="0"..Easting end for i=1,nN do Northing="0"..Northing end -- Return MGRS string. return string.format("%s %s %s %s", MGRS.UTMZone, MGRS.MGRSDigraph, string.sub(Easting, 1, acc), string.sub(Northing, 1, acc)) end end --- From http://lua-users.org/wiki/SimpleRound -- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place function UTILS.Round( num, idp ) local mult = 10 ^ ( idp or 0 ) return math.floor( num * mult + 0.5 ) / mult end --- Porting in Slmod's dostring - execute a string as LUA code with error handling. -- @param #string s The code as string to be executed -- @return #boolean success If true, code was successfully executed, else false -- @return #string Outcome Code outcome if successful or error string if not successful function UTILS.DoString( s ) local f, err = loadstring( s ) if f then return true, f() else return false, err end end --- Here is a customized version of pairs, which I called spairs because it iterates over the table in a sorted order. -- @param #table t The table -- @param #string order (Optional) The sorting function -- @return #string key The index key -- @return #string value The value at the indexed key -- @usage -- for key,value in UTILS.spairs(mytable) do -- -- your code here -- end function UTILS.spairs( t, order ) -- collect the keys local keys = {} for k in pairs(t) do keys[#keys+1] = k end -- if order function given, sort by it by passing the table and keys a, b, -- otherwise just sort the keys if order then table.sort(keys, function(a,b) return order(t, a, b) end) else table.sort(keys) end -- return the iterator function local i = 0 return function() i = i + 1 if keys[i] then return keys[i], t[keys[i]] end end end --- Here is a customized version of pairs, which I called kpairs because it iterates over the table in a sorted order, based on a function that will determine the keys as reference first. -- @param #table t The table -- @param #string getkey The function to determine the keys for sorting -- @param #string order (Optional) The sorting function itself -- @return #string key The index key -- @return #string value The value at the indexed key -- @usage -- for key,value in UTILS.kpairs(mytable, getkeyfunc) do -- -- your code here -- end function UTILS.kpairs( t, getkey, order ) -- collect the keys local keys = {} local keyso = {} for k, o in pairs(t) do keys[#keys+1] = k keyso[#keyso+1] = getkey( o ) end -- if order function given, sort by it by passing the table and keys a, b, -- otherwise just sort the keys if order then table.sort(keys, function(a,b) return order(t, a, b) end) else table.sort(keys) end -- return the iterator function local i = 0 return function() i = i + 1 if keys[i] then return keyso[i], t[keys[i]] end end end --- Here is a customized version of pairs, which I called rpairs because it iterates over the table in a random order. -- @param #table t The table -- @return #string key The index key -- @return #string value The value at the indexed key -- @usage -- for key,value in UTILS.rpairs(mytable) do -- -- your code here -- end function UTILS.rpairs( t ) -- collect the keys local keys = {} for k in pairs(t) do keys[#keys+1] = k end local random = {} local j = #keys for i = 1, j do local k = math.random( 1, #keys ) random[i] = keys[k] table.remove( keys, k ) end -- return the iterator function local i = 0 return function() i = i + 1 if random[i] then return random[i], t[random[i]] end end end -- get a new mark ID for markings function UTILS.GetMarkID() UTILS._MarkID = UTILS._MarkID + 1 return UTILS._MarkID end --- Remove an object (marker, circle, arrow, text, quad, ...) on the F10 map. -- @param #number MarkID Unique ID of the object. -- @param #number Delay (Optional) Delay in seconds before the mark is removed. function UTILS.RemoveMark(MarkID, Delay) if Delay and Delay>0 then TIMER:New(UTILS.RemoveMark, MarkID):Start(Delay) else if MarkID then trigger.action.removeMark(MarkID) end end end -- Test if a Vec2 is in a radius of another Vec2 function UTILS.IsInRadius( InVec2, Vec2, Radius ) local InRadius = ( ( InVec2.x - Vec2.x ) ^2 + ( InVec2.y - Vec2.y ) ^2 ) ^ 0.5 <= Radius return InRadius end -- Test if a Vec3 is in the sphere of another Vec3 function UTILS.IsInSphere( InVec3, Vec3, Radius ) local InSphere = ( ( InVec3.x - Vec3.x ) ^2 + ( InVec3.y - Vec3.y ) ^2 + ( InVec3.z - Vec3.z ) ^2 ) ^ 0.5 <= Radius return InSphere end --- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s. -- @param #number speed Wind speed in m/s. -- @return #number Beaufort number. -- @return #string Beauford wind description. function UTILS.BeaufortScale(speed) local bn=nil local bd=nil if speed<0.51 then bn=0 bd="Calm" elseif speed<2.06 then bn=1 bd="Light Air" elseif speed<3.60 then bn=2 bd="Light Breeze" elseif speed<5.66 then bn=3 bd="Gentle Breeze" elseif speed<8.23 then bn=4 bd="Moderate Breeze" elseif speed<11.32 then bn=5 bd="Fresh Breeze" elseif speed<14.40 then bn=6 bd="Strong Breeze" elseif speed<17.49 then bn=7 bd="Moderate Gale" elseif speed<21.09 then bn=8 bd="Fresh Gale" elseif speed<24.69 then bn=9 bd="Strong Gale" elseif speed<28.81 then bn=10 bd="Storm" elseif speed<32.92 then bn=11 bd="Violent Storm" else bn=12 bd="Hurricane" end return bn,bd end --- Split string at separators. C.f. [split-string-in-lua](http://stackoverflow.com/questions/1426954/split-string-in-lua). -- @param #string str Sting to split. -- @param #string sep Separator for split. -- @return #table Split text. function UTILS.Split(str, sep) local result = {} local regex = ("([^%s]+)"):format(sep) for each in str:gmatch(regex) do table.insert(result, each) end return result end --- Get a table of all characters in a string. -- @param #string str Sting. -- @return #table Individual characters. function UTILS.GetCharacters(str) local chars={} for i=1,#str do local c=str:sub(i,i) table.insert(chars, c) end return chars end --- Convert time in seconds to hours, minutes and seconds. -- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function. -- @param #boolean short (Optional) If true, use short output, i.e. (HH:)MM:SS without day. -- @return #string Time in format Hours:Minutes:Seconds+Days (HH:MM:SS+D). function UTILS.SecondsToClock(seconds, short) -- Nil check. if seconds==nil then return nil end -- Seconds local seconds = tonumber(seconds) -- Seconds of this day. local _seconds=seconds%(60*60*24) if seconds<0 then return nil else local hours = string.format("%02.f", math.floor(_seconds/3600)) local mins = string.format("%02.f", math.floor(_seconds/60 - (hours*60))) local secs = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60)) local days = string.format("%d", seconds/(60*60*24)) local clock=hours..":"..mins..":"..secs.."+"..days if short then if hours=="00" then --clock=mins..":"..secs clock=hours..":"..mins..":"..secs else clock=hours..":"..mins..":"..secs end end return clock end end --- Seconds of today. -- @return #number Seconds passed since last midnight. function UTILS.SecondsOfToday() -- Time in seconds. local time=timer.getAbsTime() -- Short format without days since mission start. local clock=UTILS.SecondsToClock(time, true) -- Time is now the seconds passed since last midnight. return UTILS.ClockToSeconds(clock) end --- Cound seconds until next midnight. -- @return #number Seconds to midnight. function UTILS.SecondsToMidnight() return 24*60*60-UTILS.SecondsOfToday() end --- Convert clock time from hours, minutes and seconds to seconds. -- @param #string clock String of clock time. E.g., "06:12:35" or "5:1:30+1". Format is (H)H:(M)M:((S)S)(+D) H=Hours, M=Minutes, S=Seconds, D=Days. -- @return #number Seconds. Corresponds to what you cet from timer.getAbsTime() function. function UTILS.ClockToSeconds(clock) -- Nil check. if clock==nil then return nil end -- Seconds init. local seconds=0 -- Split additional days. local dsplit=UTILS.Split(clock, "+") -- Convert days to seconds. if #dsplit>1 then seconds=seconds+tonumber(dsplit[2])*60*60*24 end -- Split hours, minutes, seconds local tsplit=UTILS.Split(dsplit[1], ":") -- Get time in seconds local i=1 for _,time in ipairs(tsplit) do if i==1 then -- Hours seconds=seconds+tonumber(time)*60*60 elseif i==2 then -- Minutes seconds=seconds+tonumber(time)*60 elseif i==3 then -- Seconds seconds=seconds+tonumber(time) end i=i+1 end return seconds end --- Display clock and mission time on screen as a message to all. -- @param #number duration Duration in seconds how long the time is displayed. Default is 5 seconds. function UTILS.DisplayMissionTime(duration) duration=duration or 5 local Tnow=timer.getAbsTime() local mission_time=Tnow-timer.getTime0() local mission_time_minutes=mission_time/60 local mission_time_seconds=mission_time%60 local local_time=UTILS.SecondsToClock(Tnow) local text=string.format("Time: %s - %02d:%02d", local_time, mission_time_minutes, mission_time_seconds) MESSAGE:New(text, duration):ToAll() end --- Replace illegal characters [<>|/?*:\\] in a string. -- @param #string Text Input text. -- @param #string ReplaceBy Replace illegal characters by this character or string. Default underscore "_". -- @return #string The input text with illegal chars replaced. function UTILS.ReplaceIllegalCharacters(Text, ReplaceBy) ReplaceBy=ReplaceBy or "_" local text=Text:gsub("[<>|/?*:\\]", ReplaceBy) return text end --- Generate a Gaussian pseudo-random number. -- @param #number x0 Expectation value of distribution. -- @param #number sigma (Optional) Standard deviation. Default 10. -- @param #number xmin (Optional) Lower cut-off value. -- @param #number xmax (Optional) Upper cut-off value. -- @param #number imax (Optional) Max number of tries to get a value between xmin and xmax (if specified). Default 100. -- @return #number Gaussian random number. function UTILS.RandomGaussian(x0, sigma, xmin, xmax, imax) -- Standard deviation. Default 10 if not given. sigma=sigma or 10 -- Max attempts. imax=imax or 100 local r local gotit=false local i=0 while not gotit do -- Uniform numbers in [0,1). We need two. local x1=math.random() local x2=math.random() -- Transform to Gaussian exp(-(x-x0)°/(2*sigma°). r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0 i=i+1 if (r>=xmin and r<=xmax) or i>imax then gotit=true end end return r end --- Randomize a value by a certain amount. -- @param #number value The value which should be randomized -- @param #number fac Randomization factor. -- @param #number lower (Optional) Lower limit of the returned value. -- @param #number upper (Optional) Upper limit of the returned value. -- @return #number Randomized value. -- @usage UTILS.Randomize(100, 0.1) returns a value between 90 and 110, i.e. a plus/minus ten percent variation. -- @usage UTILS.Randomize(100, 0.5, nil, 120) returns a value between 50 and 120, i.e. a plus/minus fivty percent variation with upper bound 120. function UTILS.Randomize(value, fac, lower, upper) local min if lower then min=math.max(value-value*fac, lower) else min=value-value*fac end local max if upper then max=math.min(value+value*fac, upper) else max=value+value*fac end local r=math.random(min, max) return r end --- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two vectors. The result is a number. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @return #number Scalar product of the two vectors a*b. function UTILS.VecDot(a, b) return a.x*b.x + a.y*b.y + a.z*b.z end --- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two 2D vectors. The result is a number. -- @param DCS#Vec2 a Vector in 2D with x, y components. -- @param DCS#Vec2 b Vector in 2D with x, y components. -- @return #number Scalar product of the two vectors a*b. function UTILS.Vec2Dot(a, b) return a.x*b.x + a.y*b.y end --- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 3D vector. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @return #number Norm of the vector. function UTILS.VecNorm(a) return math.sqrt(UTILS.VecDot(a, a)) end --- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 2D vector. -- @param DCS#Vec2 a Vector in 2D with x, y components. -- @return #number Norm of the vector. function UTILS.Vec2Norm(a) return math.sqrt(UTILS.Vec2Dot(a, a)) end --- Calculate the distance between two 2D vectors. -- @param DCS#Vec2 a Vector in 2D with x, y components. -- @param DCS#Vec2 b Vector in 2D with x, y components. -- @return #number Distance between the vectors. function UTILS.VecDist2D(a, b) local d = math.huge if (not a) or (not b) then return d end local c={x=b.x-a.x, y=b.y-a.y} d=math.sqrt(c.x*c.x+c.y*c.y) return d end --- Calculate the distance between two 3D vectors. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @return #number Distance between the vectors. function UTILS.VecDist3D(a, b) local d = math.huge if (not a) or (not b) then return d end local c={x=b.x-a.x, y=b.y-a.y, z=b.z-a.z} d=math.sqrt(UTILS.VecDot(c, c)) return d end --- Calculate the [cross product](https://en.wikipedia.org/wiki/Cross_product) of two 3D vectors. The result is a 3D vector. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @return DCS#Vec3 Vector function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} end --- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z. function UTILS.VecSubstract(a, b) return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} end --- Substract is not a word, don't want to rename the original function because it's been around since forever function UTILS.VecSubtract(a, b) return UTILS.VecSubstract(a, b) end --- Calculate the difference between two 2D vectors by substracting the x,y components from each other. -- @param DCS#Vec2 a Vector in 2D with x, y components. -- @param DCS#Vec2 b Vector in 2D with x, y components. -- @return DCS#Vec2 Vector c=a-b with c(i)=a(i)-b(i), i=x,y. function UTILS.Vec2Substract(a, b) return {x=a.x-b.x, y=a.y-b.y} end --- Substract is not a word, don't want to rename the original function because it's been around since forever function UTILS.Vec2Subtract(a, b) return UTILS.Vec2Substract(a, b) end --- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @return DCS#Vec3 Vector c=a+b with c(i)=a(i)+b(i), i=x,y,z. function UTILS.VecAdd(a, b) return {x=a.x+b.x, y=a.y+b.y, z=a.z+b.z} end --- Calculate the total vector of two 2D vectors by adding the x,y components of each other. -- @param DCS#Vec2 a Vector in 2D with x, y components. -- @param DCS#Vec2 b Vector in 2D with x, y components. -- @return DCS#Vec2 Vector c=a+b with c(i)=a(i)+b(i), i=x,y. function UTILS.Vec2Add(a, b) return {x=a.x+b.x, y=a.y+b.y} end --- Calculate the angle between two 3D vectors. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product). function UTILS.VecAngle(a, b) local cosalpha=UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b)) local alpha=0 if cosalpha>=0.9999999999 then --acos(1) is not defined. alpha=0 elseif cosalpha<=-0.999999999 then --acos(-1) is not defined. alpha=math.pi else alpha=math.acos(cosalpha) end return math.deg(alpha) end --- Calculate "heading" of a 3D vector in the X-Z plane. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @return #number Heading in degrees in [0,360). function UTILS.VecHdg(a) local h=math.deg(math.atan2(a.z, a.x)) if h<0 then h=h+360 end return h end --- Calculate "heading" of a 2D vector in the X-Y plane. -- @param DCS#Vec2 a Vector in 2D with x, y components. -- @return #number Heading in degrees in [0,360). function UTILS.Vec2Hdg(a) local h=math.deg(math.atan2(a.y, a.x)) if h<0 then h=h+360 end return h end --- Calculate the difference between two "heading", i.e. angles in [0,360) deg. -- @param #number h1 Heading one. -- @param #number h2 Heading two. -- @return #number Heading difference in degrees. function UTILS.HdgDiff(h1, h2) -- Angle in rad. local alpha= math.rad(tonumber(h1)) local beta = math.rad(tonumber(h2)) -- Runway vector. local v1={x=math.cos(alpha), y=0, z=math.sin(alpha)} local v2={x=math.cos(beta), y=0, z=math.sin(beta)} local delta=UTILS.VecAngle(v1, v2) return math.abs(delta) end --- Returns the heading from one vec3 to another vec3. -- @param DCS#Vec3 a From vec3. -- @param DCS#Vec3 b To vec3. -- @return #number Heading in degrees. function UTILS.HdgTo(a, b) local dz=b.z-a.z local dx=b.x-a.x local heading=math.deg(math.atan2(dz, dx)) if heading < 0 then heading = 360 + heading end return heading end --- Translate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param #number distance The distance to translate. -- @param #number angle Rotation angle in degrees. -- @return DCS#Vec3 Vector rotated in the (x,z) plane. function UTILS.VecTranslate(a, distance, angle) local SX = a.x local SY = a.z local Radians=math.rad(angle or 0) local TX=distance*math.cos(Radians)+SX local TY=distance*math.sin(Radians)+SY return {x=TX, y=a.y, z=TY} end --- Translate 2D vector in the 2D (x,z) plane. -- @param DCS#Vec2 a Vector in 2D with x, y components. -- @param #number distance The distance to translate. -- @param #number angle Rotation angle in degrees. -- @return DCS#Vec2 Translated vector. function UTILS.Vec2Translate(a, distance, angle) local SX = a.x local SY = a.y local Radians=math.rad(angle or 0) local TX=distance*math.cos(Radians)+SX local TY=distance*math.sin(Radians)+SY return {x=TX, y=TY} end --- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param #number angle Rotation angle in degrees. -- @return DCS#Vec3 Vector rotated in the (x,z) plane. function UTILS.Rotate2D(a, angle) local phi=math.rad(angle) local x=a.z local y=a.x local Z=x*math.cos(phi)-y*math.sin(phi) local X=x*math.sin(phi)+y*math.cos(phi) local Y=a.y local A={x=X, y=Y, z=Z} return A end --- Rotate 2D vector in the 2D (x,z) plane. -- @param DCS#Vec2 a Vector in 2D with x, y components. -- @param #number angle Rotation angle in degrees. -- @return DCS#Vec2 Vector rotated in the (x,y) plane. function UTILS.Vec2Rotate2D(a, angle) local phi=math.rad(angle) local x=a.x local y=a.y local X=x*math.cos(phi)-y*math.sin(phi) local Y=x*math.sin(phi)+y*math.cos(phi) local A={x=X, y=Y} return A end --- Converts a TACAN Channel/Mode couple into a frequency in Hz. -- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". -- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X". -- @return #number Frequency in Hz or #nil if parameters are invalid. function UTILS.TACANToFrequency(TACANChannel, TACANMode) if type(TACANChannel) ~= "number" then return nil -- error in arguments end if TACANMode ~= "X" and TACANMode ~= "Y" then return nil -- error in arguments end -- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. -- I have no idea what it does but it seems to work local A = 1151 -- 'X', channel >= 64 local B = 64 -- channel >= 64 if TACANChannel < 64 then B = 1 end if TACANMode == 'Y' then A = 1025 if TACANChannel < 64 then A = 1088 end else -- 'X' if TACANChannel < 64 then A = 962 end end return (A + TACANChannel - B) * 1000000 end --- Returns the DCS map/theatre as optained by `env.mission.theatre`. -- @return #string DCS map name. function UTILS.GetDCSMap() return env.mission.theatre end --- Returns the mission date. This is the date the mission **started**. -- @return #string Mission date in yyyy/mm/dd format. -- @return #number The year anno domini. -- @return #number The month. -- @return #number The day. function UTILS.GetDCSMissionDate() local year=tostring(env.mission.date.Year) local month=tostring(env.mission.date.Month) local day=tostring(env.mission.date.Day) return string.format("%s/%s/%s", year, month, day), tonumber(year), tonumber(month), tonumber(day) end --- Returns the day of the mission. -- @param #number Time (Optional) Abs. time in seconds. Default now, i.e. the value return from timer.getAbsTime(). -- @return #number Day of the mission. Mission starts on day 0. function UTILS.GetMissionDay(Time) Time=Time or timer.getAbsTime() local clock=UTILS.SecondsToClock(Time, false) local x=tonumber(UTILS.Split(clock, "+")[2]) return x end --- Returns the current day of the year of the mission. -- @param #number Time (Optional) Abs. time in seconds. Default now, i.e. the value return from timer.getAbsTime(). -- @return #number Current day of year of the mission. For example, January 1st returns 0, January 2nd returns 1 etc. function UTILS.GetMissionDayOfYear(Time) local Date, Year, Month, Day=UTILS.GetDCSMissionDate() local d=UTILS.GetMissionDay(Time) return UTILS.GetDayOfYear(Year, Month, Day)+d end --- Returns the magnetic declination of the map. -- Returned values for the current maps are: -- -- * Caucasus +6 (East), year ~ 2011 -- * NTTR +12 (East), year ~ 2011 -- * Normandy -10 (West), year ~ 1944 -- * Persian Gulf +2 (East), year ~ 2011 -- * The Cannel Map -10 (West) -- * Syria +5 (East) -- * Mariana Islands +2 (East) -- * Falklands +12 (East) - note there's a LOT of deviation across the map, as we're closer to the South Pole -- * Sinai +4.8 (East) -- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre -- @return #number Declination in degrees. function UTILS.GetMagneticDeclination(map) -- Map. map=map or UTILS.GetDCSMap() local declination=0 if map==DCSMAP.Caucasus then declination=6 elseif map==DCSMAP.NTTR then declination=12 elseif map==DCSMAP.Normandy then declination=-10 elseif map==DCSMAP.PersianGulf then declination=2 elseif map==DCSMAP.TheChannel then declination=-10 elseif map==DCSMAP.Syria then declination=5 elseif map==DCSMAP.MarianaIslands then declination=2 elseif map==DCSMAP.Falklands then declination=12 elseif map==DCSMAP.Sinai then declination=4.8 else declination=0 end return declination end --- Checks if a file exists or not. This requires **io** to be desanitized. -- @param #string file File that should be checked. -- @return #boolean True if the file exists, false if the file does not exist or nil if the io module is not available and the check could not be performed. function UTILS.FileExists(file) if io then local f=io.open(file, "r") if f~=nil then io.close(f) return true else return false end else return nil end end --- Checks the current memory usage collectgarbage("count"). Info is printed to the DCS log file. Time stamp is the current mission runtime. -- @param #boolean output If true, print to DCS log file. -- @return #number Memory usage in kByte. function UTILS.CheckMemory(output) local time=timer.getTime() local clock=UTILS.SecondsToClock(time) local mem=collectgarbage("count") if output then env.info(string.format("T=%s Memory usage %d kByte = %.2f MByte", clock, mem, mem/1024)) end return mem end --- Get the coalition name from its numerical ID, e.g. coalition.side.RED. -- @param #number Coalition The coalition ID. -- @return #string The coalition name, i.e. "Neutral", "Red" or "Blue" (or "Unknown"). function UTILS.GetCoalitionName(Coalition) if Coalition then if Coalition==coalition.side.BLUE then return "Blue" elseif Coalition==coalition.side.RED then return "Red" elseif Coalition==coalition.side.NEUTRAL then return "Neutral" else return "Unknown" end else return "Unknown" end end --- Get the enemy coalition for a given coalition. -- @param #number Coalition The coalition ID. -- @param #boolean Neutral Include neutral as enemy. -- @return #table Enemy coalition table. function UTILS.GetCoalitionEnemy(Coalition, Neutral) local Coalitions={} if Coalition then if Coalition==coalition.side.RED then Coalitions={coalition.side.BLUE} elseif Coalition==coalition.side.BLUE then Coalitions={coalition.side.RED} elseif Coalition==coalition.side.NEUTRAL then Coalitions={coalition.side.RED, coalition.side.BLUE} end end if Neutral then table.insert(Coalitions, coalition.side.NEUTRAL) end return Coalitions end --- Get the modulation name from its numerical value. -- @param #number Modulation The modulation enumerator number. Can be either 0 or 1. -- @return #string The modulation name, i.e. "AM"=0 or "FM"=1. Anything else will return "Unknown". function UTILS.GetModulationName(Modulation) if Modulation then if Modulation==0 then return "AM" elseif Modulation==1 then return "FM" else return "Unknown" end else return "Unknown" end end --- Get the NATO reporting name of a unit type name -- @param #number Typename The type name. -- @return #string The Reporting name or "Bogey". function UTILS.GetReportingName(Typename) local typename = string.lower(Typename) for name, value in pairs(ENUMS.ReportingName.NATO) do local svalue = string.lower(value) if string.find(typename,svalue,1,true) then return name end end return "Bogey" end --- Get the callsign name from its enumerator value -- @param #number Callsign The enumerator callsign. -- @return #string The callsign name or "Ghostrider". function UTILS.GetCallsignName(Callsign) for name, value in pairs(CALLSIGN.Aircraft) do if value==Callsign then return name end end for name, value in pairs(CALLSIGN.AWACS) do if value==Callsign then return name end end for name, value in pairs(CALLSIGN.JTAC) do if value==Callsign then return name end end for name, value in pairs(CALLSIGN.Tanker) do if value==Callsign then return name end end for name, value in pairs(CALLSIGN.B1B) do if value==Callsign then return name end end for name, value in pairs(CALLSIGN.B52) do if value==Callsign then return name end end for name, value in pairs(CALLSIGN.F15E) do if value==Callsign then return name end end for name, value in pairs(CALLSIGN.F16) do if value==Callsign then return name end end for name, value in pairs(CALLSIGN.F18) do if value==Callsign then return name end end for name, value in pairs(CALLSIGN.FARP) do if value==Callsign then return name end end for name, value in pairs(CALLSIGN.TransportAircraft) do if value==Callsign then return name end end return "Ghostrider" end --- Get the time difference between GMT and local time. -- @return #number Local time difference in hours compared to GMT. E.g. Dubai is GMT+4 ==> +4 is returned. function UTILS.GMTToLocalTimeDifference() local theatre=UTILS.GetDCSMap() if theatre==DCSMAP.Caucasus then return 4 -- Caucasus UTC+4 hours elseif theatre==DCSMAP.PersianGulf then return 4 -- Abu Dhabi UTC+4 hours elseif theatre==DCSMAP.NTTR then return -8 -- Las Vegas UTC-8 hours elseif theatre==DCSMAP.Normandy then return 0 -- Calais UTC+1 hour elseif theatre==DCSMAP.TheChannel then return 2 -- This map currently needs +2 elseif theatre==DCSMAP.Syria then return 3 -- Damascus is UTC+3 hours elseif theatre==DCSMAP.MarianaIslands then return 10 -- Guam is UTC+10 hours. elseif theatre==DCSMAP.Falklands then return -3 -- Fireland is UTC-3 hours. elseif theatre==DCSMAP.Sinai then return 2 -- Currently map is +2 but should be +3 (DCS bug?) else BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre))) return 0 end end --- Get the day of the year. Counting starts on 1st of January. -- @param #number Year The year. -- @param #number Month The month. -- @param #number Day The day. -- @return #number The day of the year. function UTILS.GetDayOfYear(Year, Month, Day) local floor = math.floor local n1 = floor(275 * Month / 9) local n2 = floor((Month + 9) / 12) local n3 = (1 + floor((Year - 4 * floor(Year / 4) + 2) / 3)) return n1 - (n2 * n3) + Day - 30 end --- Get sunrise or sun set of a specific day of the year at a specific location. -- @param #number DayOfYear The day of the year. -- @param #number Latitude Latitude. -- @param #number Longitude Longitude. -- @param #boolean Rising If true, calc sun rise, or sun set otherwise. -- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4. -- @return #number Sun rise/set in seconds of the day. function UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, Rising, Tlocal) -- Defaults local zenith=90.83 local latitude=Latitude local longitude=Longitude local rising=Rising local n=DayOfYear Tlocal=Tlocal or 0 -- Short cuts. local rad = math.rad local deg = math.deg local floor = math.floor local frac = function(n) return n - floor(n) end local cos = function(d) return math.cos(rad(d)) end local acos = function(d) return deg(math.acos(d)) end local sin = function(d) return math.sin(rad(d)) end local asin = function(d) return deg(math.asin(d)) end local tan = function(d) return math.tan(rad(d)) end local atan = function(d) return deg(math.atan(d)) end local function fit_into_range(val, min, max) local range = max - min local count if val < min then count = floor((min - val) / range) + 1 return val + count * range elseif val >= max then count = floor((val - max) / range) + 1 return val - count * range else return val end end -- Convert the longitude to hour value and calculate an approximate time local lng_hour = longitude / 15 local t if rising then -- Rising time is desired t = n + ((6 - lng_hour) / 24) else -- Setting time is desired t = n + ((18 - lng_hour) / 24) end -- Calculate the Sun's mean anomaly local M = (0.9856 * t) - 3.289 -- Calculate the Sun's true longitude local L = fit_into_range(M + (1.916 * sin(M)) + (0.020 * sin(2 * M)) + 282.634, 0, 360) -- Calculate the Sun's right ascension local RA = fit_into_range(atan(0.91764 * tan(L)), 0, 360) -- Right ascension value needs to be in the same quadrant as L local Lquadrant = floor(L / 90) * 90 local RAquadrant = floor(RA / 90) * 90 RA = RA + Lquadrant - RAquadrant -- Right ascension value needs to be converted into hours RA = RA / 15 -- Calculate the Sun's declination local sinDec = 0.39782 * sin(L) local cosDec = cos(asin(sinDec)) -- Calculate the Sun's local hour angle local cosH = (cos(zenith) - (sinDec * sin(latitude))) / (cosDec * cos(latitude)) if rising and cosH > 1 then return "N/R" -- The sun never rises on this location on the specified date elseif cosH < -1 then return "N/S" -- The sun never sets on this location on the specified date end -- Finish calculating H and convert into hours local H if rising then H = 360 - acos(cosH) else H = acos(cosH) end H = H / 15 -- Calculate local mean time of rising/setting local T = H + RA - (0.06571 * t) - 6.622 -- Adjust back to UTC local UT = fit_into_range(T - lng_hour +Tlocal, 0, 24) return floor(UT)*60*60+frac(UT)*60*60--+Tlocal*60*60 end --- Get sun rise of a specific day of the year at a specific location. -- @param #number Day Day of the year. -- @param #number Month Month of the year. -- @param #number Year Year. -- @param #number Latitude Latitude. -- @param #number Longitude Longitude. -- @param #boolean Rising If true, calc sun rise, or sun set otherwise. -- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4. Default 0. -- @return #number Sun rise in seconds of the day. function UTILS.GetSunrise(Day, Month, Year, Latitude, Longitude, Tlocal) local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day) return UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tlocal) end --- Get sun set of a specific day of the year at a specific location. -- @param #number Day Day of the year. -- @param #number Month Month of the year. -- @param #number Year Year. -- @param #number Latitude Latitude. -- @param #number Longitude Longitude. -- @param #boolean Rising If true, calc sun rise, or sun set otherwise. -- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4. Default 0. -- @return #number Sun rise in seconds of the day. function UTILS.GetSunset(Day, Month, Year, Latitude, Longitude, Tlocal) local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day) return UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tlocal) end --- Get OS time. Needs os to be desanitized! -- @return #number Os time in seconds. function UTILS.GetOSTime() if os then local ts = 0 local t = os.date("*t") local s = t.sec local m = t.min * 60 local h = t.hour * 3600 ts = s+m+h return ts else return nil end end --- Shuffle a table accoring to Fisher Yeates algorithm --@param #table t Table to be shuffled. --@return #table Shuffled table. function UTILS.ShuffleTable(t) if t == nil or type(t) ~= "table" then BASE:I("Error in ShuffleTable: Missing or wrong type of Argument") return end math.random() math.random() math.random() local TempTable = {} for i = 1, #t do local r = math.random(1,#t) TempTable[i] = t[r] table.remove(t,r) end return TempTable end --- Get a random element of a table. --@param #table t Table. --@param #boolean replace If `true`, the drawn element is replaced, i.e. not deleted. --@return #number Table element. function UTILS.GetRandomTableElement(t, replace) if t == nil or type(t) ~= "table" then BASE:I("Error in ShuffleTable: Missing or wrong type of Argument") return end math.random() math.random() math.random() local r=math.random(#t) local element=t[r] if not replace then table.remove(t, r) end return element end --- (Helicopter) Check if one loading door is open. --@param #string unit_name Unit name to be checked --@return #boolean Outcome - true if a (loading door) is open, false if not, nil if none exists. function UTILS.IsLoadingDoorOpen( unit_name ) local unit = Unit.getByName(unit_name) if unit ~= nil then local type_name = unit:getTypeName() BASE:T("TypeName = ".. type_name) if type_name == "Mi-8MT" and (unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) < 0) then BASE:T(unit_name .. " Cargo doors are open or cargo door not present") return true end if type_name == "Mi-24P" and (unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1) then BASE:T(unit_name .. " a side door is open") return true end if type_name == "UH-1H" and (unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1) then BASE:T(unit_name .. " a side door is open ") return true end if string.find(type_name, "SA342" ) and (unit:getDrawArgumentValue(34) == 1) then BASE:T(unit_name .. " front door(s) are open or doors removed") return true end if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1215) == 1 and unit:getDrawArgumentValue(1216) == 1) then BASE:T(unit_name .. " rear doors are open") return true end if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1220) == 1 or unit:getDrawArgumentValue(1221) == 1) then BASE:T(unit_name .. " para doors are open") return true end if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1217) == 1) then BASE:T(unit_name .. " side door is open") return true end if type_name == "Bell-47" then -- bell aint got no doors so always ready to load injured soldiers BASE:T(unit_name .. " door is open") return true end if type_name == "UH-60L" and (unit:getDrawArgumentValue(401) == 1 or unit:getDrawArgumentValue(402) == 1) then BASE:T(unit_name .. " cargo door is open") return true end if type_name == "UH-60L" and (unit:getDrawArgumentValue(38) > 0 or unit:getDrawArgumentValue(400) == 1 ) then BASE:T(unit_name .. " front door(s) are open") return true end if type_name == "AH-64D_BLK_II" then BASE:T(unit_name .. " front door(s) are open") return true -- no doors on this one ;) end if type_name == "Bronco-OV-10A" then BASE:T(unit_name .. " front door(s) are open") return true -- no doors on this one ;) end return false end -- nil return nil end --- Function to generate valid FM frequencies in mHz for radio beacons (FM). -- @return #table Table of frequencies. function UTILS.GenerateFMFrequencies() local FreeFMFrequencies = {} for _first = 3, 7 do for _second = 0, 5 do for _third = 0, 9 do local _frequency = ((100 * _first) + (10 * _second) + _third) * 100000 --extra 0 because we didnt bother with 4th digit table.insert(FreeFMFrequencies, _frequency) end end end return FreeFMFrequencies end --- Function to generate valid VHF frequencies in kHz for radio beacons (FM). -- @return #table VHFrequencies function UTILS.GenerateVHFrequencies() -- known and sorted map-wise NDBs in kHz local _skipFrequencies = { 214,274,291.5,295,297.5, 300.5,304,305,307,309.5,311,312,312.5,316, 320,324,328,329,330,332,336,337, 342,343,348,351,352,353,358, 363,365,368,372.5,374, 380,381,384,385,389,395,396, 414,420,430,432,435,440,450,455,462,470,485, 507,515,520,525,528,540,550,560,570,577,580, 602,625,641,662,670,680,682,690, 705,720,722,730,735,740,745,750,770,795, 822,830,862,866, 905,907,920,935,942,950,995, 1000,1025,1030,1050,1065,1116,1175,1182,1210,1215 } local FreeVHFFrequencies = {} -- first range local _start = 200000 while _start < 400000 do -- skip existing NDB frequencies# local _found = false for _, value in pairs(_skipFrequencies) do if value * 1000 == _start then _found = true break end end if _found == false then table.insert(FreeVHFFrequencies, _start) end _start = _start + 10000 end -- second range _start = 400000 while _start < 850000 do -- skip existing NDB frequencies local _found = false for _, value in pairs(_skipFrequencies) do if value * 1000 == _start then _found = true break end end if _found == false then table.insert(FreeVHFFrequencies, _start) end _start = _start + 10000 end -- third range _start = 850000 while _start <= 999000 do -- adjusted for Gazelle -- skip existing NDB frequencies local _found = false for _, value in pairs(_skipFrequencies) do if value * 1000 == _start then _found = true break end end if _found == false then table.insert(FreeVHFFrequencies, _start) end _start = _start + 50000 end return FreeVHFFrequencies end --- Function to generate valid UHF Frequencies in mHz (AM). -- @return #table UHF Frequencies function UTILS.GenerateUHFrequencies() local FreeUHFFrequencies = {} local _start = 220000000 while _start < 399000000 do if _start ~= 243000000 then table.insert(FreeUHFFrequencies, _start) end _start = _start + 500000 end return FreeUHFFrequencies end --- Function to generate valid laser codes for JTAC. -- @return #table Laser Codes. function UTILS.GenerateLaserCodes() local jtacGeneratedLaserCodes = {} -- helper function local function ContainsDigit(_number, _numberToFind) local _thisNumber = _number local _thisDigit = 0 while _thisNumber ~= 0 do _thisDigit = _thisNumber % 10 _thisNumber = math.floor(_thisNumber / 10) if _thisDigit == _numberToFind then return true end end return false end -- generate list of laser codes local _code = 1111 local _count = 1 while _code < 1777 and _count < 30 do while true do _code = _code + 1 if not ContainsDigit(_code, 8) and not ContainsDigit(_code, 9) and not ContainsDigit(_code, 0) then table.insert(jtacGeneratedLaserCodes, _code) break end end _count = _count + 1 end return jtacGeneratedLaserCodes end --- Ensure the passed object is a table. -- @param #table Object The object that should be a table. -- @param #boolean ReturnNil If `true`, return `#nil` if `Object` is nil. Otherwise an empty table `{}` is returned. -- @return #table The object that now certainly *is* a table. function UTILS.EnsureTable(Object, ReturnNil) if Object then if type(Object)~="table" then Object={Object} end else if ReturnNil then return nil else Object={} end end return Object end --- Function to save an object to a file -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. Existing file will be overwritten. -- @param #table Data The LUA data structure to save. This will be e.g. a table of text lines with an \\n at the end of each line. -- @return #boolean outcome True if saving is possible, else false. function UTILS.SaveToFile(Path,Filename,Data) -- Thanks to @FunkyFranky -- Check io module is available. if not io then BASE:E("ERROR: io not desanitized. Can't save current file.") return false end -- Check default path. if Path==nil and not lfs then BASE:E("WARNING: lfs not desanitized. File will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end -- Set path or default. local path = nil if lfs then path=Path or lfs.writedir() end -- Set file name. local filename=Filename if path~=nil then filename=path.."\\"..filename end -- write local f = assert(io.open(filename, "wb")) f:write(Data) f:close() return true end --- Function to save an object to a file -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @return #boolean outcome True if reading is possible and successful, else false. -- @return #table data The data read from the filesystem (table of lines of text). Each line is one single #string! function UTILS.LoadFromFile(Path,Filename) -- Thanks to @FunkyFranky -- Check io module is available. if not io then BASE:E("ERROR: io not desanitized. Can't save current state.") return false end -- Check default path. if Path==nil and not lfs then BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end -- Set path or default. local path = nil if lfs then path=Path or lfs.writedir() end -- Set file name. local filename=Filename if path~=nil then filename=path.."\\"..filename end -- Check if file exists. local exists=UTILS.CheckFileExists(Path,Filename) if not exists then BASE:I(string.format("ERROR: File %s does not exist!",filename)) return false end -- read local file=assert(io.open(filename, "rb")) local loadeddata = {} for line in file:lines() do loadeddata[#loadeddata+1] = line end file:close() return true, loadeddata end --- Function to check if a file exists. -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @return #boolean outcome True if reading is possible, else false. function UTILS.CheckFileExists(Path,Filename) -- Thanks to @FunkyFranky -- Function that check if a file exists. local function _fileexists(name) local f=io.open(name,"r") if f~=nil then io.close(f) return true else return false end end -- Check io module is available. if not io then BASE:E("ERROR: io not desanitized.") return false end -- Check default path. if Path==nil and not lfs then BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end -- Set path or default. local path = nil if lfs then path=Path or lfs.writedir() end -- Set file name. local filename=Filename if path~=nil then filename=path.."\\"..filename end -- Check if file exists. local exists=_fileexists(filename) if not exists then BASE:E(string.format("ERROR: File %s does not exist!",filename)) return false else return true end end --- Function to obtain a table of typenames from the group given with the number of units of the same type in the group. -- @param Wrapper.Group#GROUP Group The group to list -- @return #table Table of typnames and typename counts, e.g. `{["KAMAZ Truck"]=3,["ATZ-5"]=1}` function UTILS.GetCountPerTypeName(Group) local units = Group:GetUnits() local TypeNameTable = {} for _,_unt in pairs (units) do local unit = _unt -- Wrapper.Unit#UNIT local typen = unit:GetTypeName() if not TypeNameTable[typen] then TypeNameTable[typen] = 1 else TypeNameTable[typen] = TypeNameTable[typen] + 1 end end return TypeNameTable end --- Function to save the state of a list of groups found by name -- @param #table List Table of strings with groupnames -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @param #boolean Structured Append the data with a list of typenames in the group plus their count. -- @return #boolean outcome True if saving is successful, else false. -- @usage -- We will go through the list and find the corresponding group and save the current group size (0 when dead). -- These groups are supposed to be put on the map in the ME and have *not* moved (e.g. stationary SAM sites). -- Position is still saved for your usage. -- The idea is to reduce the number of units when reloading the data again to restart the saved mission. -- The data will be a simple comma separated list of groupname and size, with one header line. function UTILS.SaveStationaryListOfGroups(List,Path,Filename,Structured) local filename = Filename or "StateListofGroups" local data = "--Save Stationary List of Groups: "..Filename .."\n" for _,_group in pairs (List) do local group = GROUP:FindByName(_group) -- Wrapper.Group#GROUP if group and group:IsAlive() then local units = group:CountAliveUnits() local position = group:GetVec3() if Structured then local structure = UTILS.GetCountPerTypeName(group) local strucdata = "" for typen,anzahl in pairs (structure) do strucdata = strucdata .. typen .. "=="..anzahl..";" end data = string.format("%s%s,%d,%d,%d,%d,%s\n",data,_group,units,position.x,position.y,position.z,strucdata) else data = string.format("%s%s,%d,%d,%d,%d\n",data,_group,units,position.x,position.y,position.z) end else data = string.format("%s%s,0,0,0,0\n",data,_group) end end -- save the data local outcome = UTILS.SaveToFile(Path,Filename,data) return outcome end --- Function to save the state of a set of Wrapper.Group#GROUP objects. -- @param Core.Set#SET_BASE Set of objects to save -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @param #boolean Structured Append the data with a list of typenames in the group plus their count. -- @return #boolean outcome True if saving is successful, else false. -- @usage -- We will go through the set and find the corresponding group and save the current group size and current position. -- The idea is to respawn the groups **spawned during an earlier run of the mission** at the given location and reduce -- the number of units in the group when reloading the data again to restart the saved mission. Note that *dead* groups -- cannot be covered with this. -- **Note** Do NOT use dashes or hashes in group template names (-,#)! -- The data will be a simple comma separated list of groupname and size, with one header line. -- The current task/waypoint/etc cannot be restored. function UTILS.SaveSetOfGroups(Set,Path,Filename,Structured) local filename = Filename or "SetOfGroups" local data = "--Save SET of groups: "..Filename .."\n" local List = Set:GetSetObjects() for _,_group in pairs (List) do local group = _group -- Wrapper.Group#GROUP if group and group:IsAlive() then local name = group:GetName() local template = string.gsub(name,"-(.+)$","") if string.find(template,"#") then template = string.gsub(name,"#(%d+)$","") end local units = group:CountAliveUnits() local position = group:GetVec3() if Structured then local structure = UTILS.GetCountPerTypeName(group) local strucdata = "" for typen,anzahl in pairs (structure) do strucdata = strucdata .. typen .. "=="..anzahl..";" end data = string.format("%s%s,%s,%d,%d,%d,%d,%s\n",data,name,template,units,position.x,position.y,position.z,strucdata) else data = string.format("%s%s,%s,%d,%d,%d,%d\n",data,name,template,units,position.x,position.y,position.z) end end end -- save the data local outcome = UTILS.SaveToFile(Path,Filename,data) return outcome end --- Function to save the state of a set of Wrapper.Static#STATIC objects. -- @param Core.Set#SET_BASE Set of objects to save -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @return #boolean outcome True if saving is successful, else false. -- @usage -- We will go through the set and find the corresponding static and save the current name and postion when alive. -- The data will be a simple comma separated list of name and state etc, with one header line. function UTILS.SaveSetOfStatics(Set,Path,Filename) local filename = Filename or "SetOfStatics" local data = "--Save SET of statics: "..Filename .."\n" local List = Set:GetSetObjects() for _,_group in pairs (List) do local group = _group -- Wrapper.Static#STATIC if group and group:IsAlive() then local name = group:GetName() local position = group:GetVec3() data = string.format("%s%s,%d,%d,%d\n",data,name,position.x,position.y,position.z) end end -- save the data local outcome = UTILS.SaveToFile(Path,Filename,data) return outcome end --- Function to save the state of a list of statics found by name -- @param #table List Table of strings with statics names -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @return #boolean outcome True if saving is successful, else false. -- @usage -- We will go through the list and find the corresponding static and save the current alive state as 1 (0 when dead). -- Position is saved for your usage. **Note** this works on UNIT-name level. -- The idea is to reduce the number of units when reloading the data again to restart the saved mission. -- The data will be a simple comma separated list of name and state etc, with one header line. function UTILS.SaveStationaryListOfStatics(List,Path,Filename) local filename = Filename or "StateListofStatics" local data = "--Save Stationary List of Statics: "..Filename .."\n" for _,_group in pairs (List) do local group = STATIC:FindByName(_group,false) -- Wrapper.Static#STATIC if group and group:IsAlive() then local position = group:GetVec3() data = string.format("%s%s,1,%d,%d,%d\n",data,_group,position.x,position.y,position.z) else data = string.format("%s%s,0,0,0,0\n",data,_group) end end -- save the data local outcome = UTILS.SaveToFile(Path,Filename,data) return outcome end --- Load back a stationary list of groups from file. -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @param #boolean Reduce If false, existing loaded groups will not be reduced to fit the saved number. -- @param #boolean Structured (Optional, needs Reduce = true) If true, and the data has been saved as structure before, remove the correct unit types as per the saved list. -- @param #boolean Cinematic (Optional, needs Structured = true) If true, place a fire/smoke effect on the dead static position. -- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke. -- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5. -- @return #table Table of data objects (tables) containing groupname, coordinate and group object. Returns nil when file cannot be read. -- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )` function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinematic,Effect,Density) local fires = {} local function Smokers(name,coord,effect,density) local eff = math.random(8) if type(effect) == "number" then eff = effect end coord:BigSmokeAndFire(eff,density,name) table.insert(fires,name) end local function Cruncher(group,typename,anzahl) local units = group:GetUnits() local reduced = 0 for _,_unit in pairs (units) do local typo = _unit:GetTypeName() if typename == typo then if Cinematic then local coordinate = _unit:GetCoordinate() local name = _unit:GetName() Smokers(name,coordinate,Effect,Density) end _unit:Destroy(false) reduced = reduced + 1 if reduced == anzahl then break end end end end local reduce = true if Reduce == false then reduce = false end local filename = Filename or "StateListofGroups" local datatable = {} if UTILS.CheckFileExists(Path,filename) then local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) -- remove header table.remove(loadeddata, 1) for _id,_entry in pairs (loadeddata) do local dataset = UTILS.Split(_entry,",") -- groupname,units,position.x,position.y,position.z local groupname = dataset[1] local size = tonumber(dataset[2]) local posx = tonumber(dataset[3]) local posy = tonumber(dataset[4]) local posz = tonumber(dataset[5]) local structure = dataset[6] --BASE:I({structure}) local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) local data = { groupname=groupname, size=size, coordinate=coordinate, group=GROUP:FindByName(groupname) } if reduce then local actualgroup = GROUP:FindByName(groupname) if actualgroup and actualgroup:IsAlive() and actualgroup:CountAliveUnits() > size then if Structured and structure then --BASE:I("Reducing group structure!") local loadedstructure = {} local strcset = UTILS.Split(structure,";") for _,_data in pairs(strcset) do local datasplit = UTILS.Split(_data,"==") loadedstructure[datasplit[1]] = tonumber(datasplit[2]) end --BASE:I({loadedstructure}) local originalstructure = UTILS.GetCountPerTypeName(actualgroup) --BASE:I({originalstructure}) for _name,_number in pairs(originalstructure) do local loadednumber = 0 if loadedstructure[_name] then loadednumber = loadedstructure[_name] end local reduce = false if loadednumber < _number then reduce = true end --BASE:I(string.format("Looking at: %s | Original number: %d | Loaded number: %d | Reduce: %s",_name,_number,loadednumber,tostring(reduce))) if reduce then Cruncher(actualgroup,_name,_number-loadednumber) end end else local reduction = actualgroup:CountAliveUnits() - size --BASE:I("Reducing groupsize by ".. reduction .. " units!") -- reduce existing group local units = actualgroup:GetUnits() local units2 = UTILS.ShuffleTable(units) -- randomize table for i=1,reduction do units2[i]:Destroy(false) end end end end table.insert(datatable,data) end else return nil end return datatable,fires end --- Load back a SET of groups from file. -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @param #boolean Spawn If set to false, do not re-spawn the groups loaded in location and reduce to size. -- @param #boolean Structured (Optional, needs Spawn=true)If true, and the data has been saved as structure before, remove the correct unit types as per the saved list. -- @param #boolean Cinematic (Optional, needs Structured=true) If true, place a fire/smoke effect on the dead static position. -- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke. -- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5. -- @return Core.Set#SET_GROUP Set of GROUP objects. -- Returns nil when file cannot be read. Returns a table of data entries if Spawn is false: `{ groupname=groupname, size=size, coordinate=coordinate, template=template }` -- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )` function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,Density) local fires = {} local usedtemplates = {} local spawn = true if Spawn == false then spawn = false end local filename = Filename or "SetOfGroups" local setdata = SET_GROUP:New() local datatable = {} local function Smokers(name,coord,effect,density) local eff = math.random(8) if type(effect) == "number" then eff = effect end coord:BigSmokeAndFire(eff,density,name) table.insert(fires,name) end local function Cruncher(group,typename,anzahl) local units = group:GetUnits() local reduced = 0 for _,_unit in pairs (units) do local typo = _unit:GetTypeName() if typename == typo then if Cinematic then local coordinate = _unit:GetCoordinate() local name = _unit:GetName() Smokers(name,coordinate,Effect,Density) end _unit:Destroy(false) reduced = reduced + 1 if reduced == anzahl then break end end end end local function PostSpawn(args) local spwndgrp = args[1] local size = args[2] local structure = args[3] setdata:AddObject(spwndgrp) local actualsize = spwndgrp:CountAliveUnits() if actualsize > size then if Structured and structure then local loadedstructure = {} local strcset = UTILS.Split(structure,";") for _,_data in pairs(strcset) do local datasplit = UTILS.Split(_data,"==") loadedstructure[datasplit[1]] = tonumber(datasplit[2]) end local originalstructure = UTILS.GetCountPerTypeName(spwndgrp) for _name,_number in pairs(originalstructure) do local loadednumber = 0 if loadedstructure[_name] then loadednumber = loadedstructure[_name] end local reduce = false if loadednumber < _number then reduce = true end if reduce then Cruncher(spwndgrp,_name,_number-loadednumber) end end else local reduction = actualsize-size -- reduce existing group local units = spwndgrp:GetUnits() local units2 = UTILS.ShuffleTable(units) -- randomize table for i=1,reduction do units2[i]:Destroy(false) end end end end local function MultiUse(Data) local template = Data.template if template and usedtemplates[template] and usedtemplates[template].used and usedtemplates[template].used > 1 then -- multispawn if not usedtemplates[template].done then local spwnd = 0 local spawngrp = SPAWN:New(template) spawngrp:InitLimit(0,usedtemplates[template].used) for _,_entry in pairs(usedtemplates[template].data) do spwnd = spwnd + 1 local sgrp=spawngrp:SpawnFromCoordinate(_entry.coordinate,spwnd) BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure}) end usedtemplates[template].done = true end return true else return false end end --BASE:I("Spawn = "..tostring(spawn)) if UTILS.CheckFileExists(Path,filename) then local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) -- remove header table.remove(loadeddata, 1) for _id,_entry in pairs (loadeddata) do local dataset = UTILS.Split(_entry,",") -- groupname,template,units,position.x,position.y,position.z local groupname = dataset[1] local template = dataset[2] local size = tonumber(dataset[3]) local posx = tonumber(dataset[4]) local posy = tonumber(dataset[5]) local posz = tonumber(dataset[6]) local structure = dataset[7] local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) local group=nil if size > 0 then local data = { groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure } table.insert(datatable,data) if usedtemplates[template] then usedtemplates[template].used = usedtemplates[template].used + 1 table.insert(usedtemplates[template].data,data) else usedtemplates[template] = { data = {}, used = 1, done = false, } table.insert(usedtemplates[template].data,data) end end end for _id,_entry in pairs (datatable) do if spawn and not MultiUse(_entry) and _entry.size > 0 then local group = SPAWN:New(_entry.template) local sgrp=group:SpawnFromCoordinate(_entry.coordinate) BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure}) end end else return nil end if spawn then return setdata,fires else return datatable end end --- Load back a SET of statics from file. -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @return Core.Set#SET_STATIC Set SET_STATIC containing the static objects. function UTILS.LoadSetOfStatics(Path,Filename) local filename = Filename or "SetOfStatics" local datatable = SET_STATIC:New() if UTILS.CheckFileExists(Path,filename) then local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) -- remove header table.remove(loadeddata, 1) for _id,_entry in pairs (loadeddata) do local dataset = UTILS.Split(_entry,",") local staticname = dataset[1] local StaticObject = STATIC:FindByName(staticname,false) if StaticObject then datatable:AddObject(StaticObject) end end else return nil end return datatable end --- Load back a stationary list of statics from file. -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @param #boolean Reduce If false, do not destroy the units with size=0. -- @param #boolean Dead (Optional, needs Reduce = true) If Dead is true, re-spawn the dead object as dead and do not just delete it. -- @param #boolean Cinematic (Optional, needs Dead = true) If true, place a fire/smoke effect on the dead static position. -- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke. -- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5. -- @return #table Table of data objects (tables) containing staticname, size (0=dead else 1), coordinate and the static object. Dead objects will have coordinate points `{x=0,y=0,z=0}` -- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )` -- Returns nil when file cannot be read. function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce,Dead,Cinematic,Effect,Density) local fires = {} local reduce = true if Reduce == false then reduce = false end local filename = Filename or "StateListofStatics" local datatable = {} if UTILS.CheckFileExists(Path,filename) then local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) -- remove header table.remove(loadeddata, 1) for _id,_entry in pairs (loadeddata) do local dataset = UTILS.Split(_entry,",") -- staticname,units(1/0),position.x,position.y,position.z) local staticname = dataset[1] local size = tonumber(dataset[2]) local posx = tonumber(dataset[3]) local posy = tonumber(dataset[4]) local posz = tonumber(dataset[5]) local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) local data = { staticname=staticname, size=size, coordinate=coordinate, static=STATIC:FindByName(staticname,false) } table.insert(datatable,data) if size==0 and reduce then local static = STATIC:FindByName(staticname,false) if static then if Dead then local deadobject = SPAWNSTATIC:NewFromStatic(staticname,static:GetCountry()) deadobject:InitDead(true) local heading = static:GetHeading() local coord = static:GetCoordinate() static:Destroy(false) deadobject:SpawnFromCoordinate(coord,heading,staticname) if Cinematic then local effect = math.random(8) if type(Effect) == "number" then effect = Effect end coord:BigSmokeAndFire(effect,Density,staticname) table.insert(fires,staticname) end else static:Destroy(false) end end end end else return nil end return datatable,fires end --- Heading Degrees (0-360) to Cardinal -- @param #number Heading The heading -- @return #string Cardinal, e.g. "NORTH" function UTILS.BearingToCardinal(Heading) if Heading >= 0 and Heading <= 22 then return "North" elseif Heading >= 23 and Heading <= 66 then return "North-East" elseif Heading >= 67 and Heading <= 101 then return "East" elseif Heading >= 102 and Heading <= 146 then return "South-East" elseif Heading >= 147 and Heading <= 201 then return "South" elseif Heading >= 202 and Heading <= 246 then return "South-West" elseif Heading >= 247 and Heading <= 291 then return "West" elseif Heading >= 292 and Heading <= 338 then return "North-West" elseif Heading >= 339 then return "North" end end --- Create a BRAA NATO call string BRAA between two GROUP objects -- @param Wrapper.Group#GROUP FromGrp GROUP object -- @param Wrapper.Group#GROUP ToGrp GROUP object -- @return #string Formatted BRAA NATO call function UTILS.ToStringBRAANATO(FromGrp,ToGrp) local BRAANATO = "Merged." local GroupNumber = ToGrp:GetSize() local GroupWords = "Singleton" if GroupNumber == 2 then GroupWords = "Two-Ship" elseif GroupNumber >= 3 then GroupWords = "Heavy" end local grpLeadUnit = ToGrp:GetUnit(1) local tgtCoord = grpLeadUnit:GetCoordinate() local currentCoord = FromGrp:GetCoordinate() local hdg = UTILS.Round(ToGrp:GetHeading()/100,1)*100 local bearing = UTILS.Round(currentCoord:HeadingTo(tgtCoord),0) local rangeMetres = tgtCoord:Get2DDistance(currentCoord) local rangeNM = UTILS.Round( UTILS.MetersToNM(rangeMetres), 0) local aspect = tgtCoord:ToStringAspect(currentCoord) local alt = UTILS.Round(UTILS.MetersToFeet(grpLeadUnit:GetAltitude())/1000,0)--*1000 local track = UTILS.BearingToCardinal(hdg) if rangeNM > 3 then if aspect == "" then BRAANATO = string.format("%s, BRA, %03d, %d miles, Angels %d, Track %s",GroupWords,bearing, rangeNM, alt, track) else BRAANATO = string.format("%s, BRAA, %03d, %d miles, Angels %d, %s, Track %s",GroupWords, bearing, rangeNM, alt, aspect, track) end end return BRAANATO end --- Check if an object is contained in a table. -- @param #table Table The table. -- @param #table Object The object to check. -- @param #string Key (Optional) Key to check. By default, the object itself is checked. -- @return #booolen Returns `true` if object is in table. function UTILS.IsInTable(Table, Object, Key) for key, object in pairs(Table) do if Key then if Object[Key]==object[Key] then return true end else if object==Object then return true end end end return false end --- Check if any object of multiple given objects is contained in a table. -- @param #table Table The table. -- @param #table Objects The objects to check. -- @param #string Key (Optional) Key to check. -- @return #booolen Returns `true` if object is in table. function UTILS.IsAnyInTable(Table, Objects, Key) for _,Object in pairs(UTILS.EnsureTable(Objects)) do for key, object in pairs(Table) do if Key then if Object[Key]==object[Key] then return true end else if object==Object then return true end end end end return false end --- Helper function to plot a racetrack on the F10 Map - curtesy of Buur. -- @param Core.Point#COORDINATE Coordinate -- @param #number Altitude Altitude in feet -- @param #number Speed Speed in knots -- @param #number Heading Heading in degrees -- @param #number Leg Leg in NM -- @param #number Coalition Coalition side, e.g. coaltion.side.RED or coaltion.side.BLUE -- @param #table Color Color of the line in RGB, e.g. {1,0,0} for red -- @param #number Alpha Transparency factor, between 0.1 and 1 -- @param #number LineType Line type to be used, line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly function UTILS.PlotRacetrack(Coordinate, Altitude, Speed, Heading, Leg, Coalition, Color, Alpha, LineType, ReadOnly) local fix_coordinate = Coordinate local altitude = Altitude local speed = Speed or 350 local heading = Heading or 270 local leg_distance = Leg or 10 local coalition = Coalition or -1 local color = Color or {1,0,0} local alpha = Alpha or 1 local lineType = LineType or 1 speed = UTILS.IasToTas(speed, UTILS.FeetToMeters(altitude), oatcorr) local turn_radius = 0.0211 * speed -3.01 local point_two = fix_coordinate:Translate(UTILS.NMToMeters(leg_distance), heading, true, false) local point_three = point_two:Translate(UTILS.NMToMeters(turn_radius)*2, heading - 90, true, false) local point_four = fix_coordinate:Translate(UTILS.NMToMeters(turn_radius)*2, heading - 90, true, false) local circle_center_fix_four = point_two:Translate(UTILS.NMToMeters(turn_radius), heading - 90, true, false) local circle_center_two_three = fix_coordinate:Translate(UTILS.NMToMeters(turn_radius), heading - 90, true, false) fix_coordinate:LineToAll(point_two, coalition, color, alpha, lineType) point_four:LineToAll(point_three, coalition, color, alpha, lineType) circle_center_fix_four:CircleToAll(UTILS.NMToMeters(turn_radius), coalition, color, alpha, nil, 0, lineType)--, ReadOnly, Text) circle_center_two_three:CircleToAll(UTILS.NMToMeters(turn_radius), coalition, color, alpha, nil, 0, lineType)--, ReadOnly, Text) end --- Get the current time in a "nice" format like 21:01:15 -- @return #string Returns string with the current time function UTILS.TimeNow() return UTILS.SecondsToClock(timer.getAbsTime(), false, false) end --- Given 2 "nice" time string, returns the difference between the two in seconds -- @param #string start_time Time string like "07:15:22" -- @param #string end_time Time string like "08:11:27" -- @return #number Seconds between start_time and end_time function UTILS.TimeDifferenceInSeconds(start_time, end_time) return UTILS.ClockToSeconds(end_time) - UTILS.ClockToSeconds(start_time) end --- Check if the current time is later than time_string. -- @param #string start_time Time string like "07:15:22" -- @return #boolean True if later, False if before function UTILS.TimeLaterThan(time_string) if timer.getAbsTime() > UTILS.ClockToSeconds(time_string) then return true end return false end --- Check if the current time is before time_string. -- @param #string start_time Time string like "07:15:22" -- @return #boolean False if later, True if before function UTILS.TimeBefore(time_string) if timer.getAbsTime() < UTILS.ClockToSeconds(time_string) then return true end return false end --- Combines two time strings to give you a new time. For example "15:16:32" and "02:06:24" would return "17:22:56" -- @param #string time_string_01 Time string like "07:15:22" -- @param #string time_string_02 Time string like "08:11:27" -- @return #string Result of the two time string combined function UTILS.CombineTimeStrings(time_string_01, time_string_02) local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)") local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)") local total_seconds = tonumber(seconds1) + tonumber(seconds2) + tonumber(minutes1) * 60 + tonumber(minutes2) * 60 + tonumber(hours1) * 3600 + tonumber(hours2) * 3600 total_seconds = total_seconds % (24 * 3600) if total_seconds < 0 then total_seconds = total_seconds + 24 * 3600 end local hours = math.floor(total_seconds / 3600) total_seconds = total_seconds - hours * 3600 local minutes = math.floor(total_seconds / 60) local seconds = total_seconds % 60 return string.format("%02d:%02d:%02d", hours, minutes, seconds) end --- Subtracts two time string to give you a new time. For example "15:16:32" and "02:06:24" would return "13:10:08" -- @param #string time_string_01 Time string like "07:15:22" -- @param #string time_string_02 Time string like "08:11:27" -- @return #string Result of the two time string subtracted function UTILS.SubtractTimeStrings(time_string_01, time_string_02) local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)") local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)") local total_seconds = tonumber(seconds1) - tonumber(seconds2) + tonumber(minutes1) * 60 - tonumber(minutes2) * 60 + tonumber(hours1) * 3600 - tonumber(hours2) * 3600 total_seconds = total_seconds % (24 * 3600) if total_seconds < 0 then total_seconds = total_seconds + 24 * 3600 end local hours = math.floor(total_seconds / 3600) total_seconds = total_seconds - hours * 3600 local minutes = math.floor(total_seconds / 60) local seconds = total_seconds % 60 return string.format("%02d:%02d:%02d", hours, minutes, seconds) end --- Checks if the current time is in between start_time and end_time -- @param #string time_string_01 Time string like "07:15:22" -- @param #string time_string_02 Time string like "08:11:27" -- @return #bool True if it is, False if it's not function UTILS.TimeBetween(start_time, end_time) return UTILS.TimeLaterThan(start_time) and UTILS.TimeBefore(end_time) end --- Easy to read one line to roll the dice on something. 1% is very unlikely to happen, 99% is very likely to happen -- @param #number chance (optional) Percentage chance you want something to happen. Defaults to a random number if not given -- @return #bool True if the dice roll was within the given percentage chance of happening function UTILS.PercentageChance(chance) chance = chance or math.random(0, 100) chance = UTILS.Clamp(chance, 0, 100) local percentage = math.random(0, 100) if percentage < chance then return true end return false end --- Easy to read one liner to clamp a value -- @param #number value Input value -- @param #number min Minimal value that should be respected -- @param #number max Maximal value that should be respected -- @return #number Clamped value function UTILS.Clamp(value, min, max) if value < min then value = min end if value > max then value = max end return value end --- Clamp an angle so that it's always between 0 and 360 while still being correct -- @param #number value Input value -- @return #number Clamped value function UTILS.ClampAngle(value) if value > 360 then return value - 360 end if value < 0 then return value + 360 end return value end --- Remap an input to a new value in a given range. For example: --- UTILS.RemapValue(20, 10, 30, 0, 200) would return 100 --- 20 is 50% between 10 and 30 --- 50% between 0 and 200 is 100 -- @param #number value Input value -- @param #number old_min Min value to remap from -- @param #number old_max Max value to remap from -- @param #number new_min Min value to remap to -- @param #number new_max Max value to remap to -- @return #number Remapped value function UTILS.RemapValue(value, old_min, old_max, new_min, new_max) new_min = new_min or 0 new_max = new_max or 100 local old_range = old_max - old_min local new_range = new_max - new_min local percentage = (value - old_min) / old_range return (new_range * percentage) + new_min end --- Given a triangle made out of 3 vector 2s, return a vec2 that is a random number in this triangle -- @param #Vec2 pt1 Min value to remap from -- @param #Vec2 pt2 Max value to remap from -- @param #Vec2 pt3 Max value to remap from -- @return #Vec2 Random point in triangle function UTILS.RandomPointInTriangle(pt1, pt2, pt3) local pt = {math.random(), math.random()} table.sort(pt) local s = pt[1] local t = pt[2] - pt[1] local u = 1 - pt[2] return {x = s * pt1.x + t * pt2.x + u * pt3.x, y = s * pt1.y + t * pt2.y + u * pt3.y} end --- Checks if a given angle (heading) is between 2 other angles. Min and max have to be given in clockwise order For example: --- UTILS.AngleBetween(350, 270, 15) would return True --- UTILS.AngleBetween(22, 95, 20) would return False -- @param #number angle Min value to remap from -- @param #number min Max value to remap from -- @param #number max Max value to remap from -- @return #bool function UTILS.AngleBetween(angle, min, max) angle = (360 + (angle % 360)) % 360 min = (360 + min % 360) % 360 max = (360 + max % 360) % 360 if min < max then return min <= angle and angle <= max end return min <= angle or angle <= max end --- Easy to read one liner to write a JSON file. Everything in @data should be serializable --- json.lua exists in the DCS install Scripts folder -- @param #table data table to write -- @param #string file_path File path function UTILS.WriteJSON(data, file_path) package.path = package.path .. ";.\\Scripts\\?.lua" local JSON = require("json") local pretty_json_text = JSON:encode_pretty(data) local write_file = io.open(file_path, "w") write_file:write(pretty_json_text) write_file:close() end --- Easy to read one liner to read a JSON file. --- json.lua exists in the DCS install Scripts folder -- @param #string file_path File path -- @return #table function UTILS.ReadJSON(file_path) package.path = package.path .. ";.\\Scripts\\?.lua" local JSON = require("json") local read_file = io.open(file_path, "r") local contents = read_file:read( "*a" ) io.close(read_file) return JSON:decode(contents) end --- Get the properties names and values of properties set up on a Zone in the Mission Editor. --- This doesn't work for any zones created in MOOSE -- @param #string zone_name Name of the zone as set up in the Mission Editor -- @return #table with all the properties on a zone function UTILS.GetZoneProperties(zone_name) local return_table = {} for _, zone in pairs(env.mission.triggers.zones) do if zone["name"] == zone_name then if table.length(zone["properties"]) > 0 then for _, property in pairs(zone["properties"]) do return_table[property["key"]] = property["value"] end return return_table else BASE:I(string.format("%s doesn't have any properties", zone_name)) return {} end end end end --- Rotates a point around another point with a given angle. Useful if you're loading in groups or --- statics but you want to rotate them all as a collection. You can get the center point of everything --- and then rotate all the positions of every object around this center point. -- @param #Vec2 point Point that you want to rotate -- @param #Vec2 pivot Pivot point of the rotation -- @param #number angle How many degrees the point should be rotated -- @return #Vec Rotated point function UTILS.RotatePointAroundPivot(point, pivot, angle) local radians = math.rad(angle) local x = point.x - pivot.x local y = point.y - pivot.y local rotated_x = x * math.cos(radians) - y * math.sin(radians) local rotatex_y = x * math.sin(radians) + y * math.cos(radians) local original_x = rotated_x + pivot.x local original_y = rotatex_y + pivot.y return { x = original_x, y = original_y } end --- Makes a string semi-unique by attaching a random number between 0 and 1 million to it -- @param #string base String you want to unique-fy -- @return #string Unique string function UTILS.UniqueName(base) base = base or "" local ran = tostring(math.random(0, 1000000)) if base == "" then return ran end return base .. "_" .. ran end --- Check if a string starts with something -- @param #string str String to check -- @param #string value -- @return #bool True if str starts with value function string.startswith(str, value) return string.sub(str,1,string.len(value)) == value end --- Check if a string ends with something -- @param #string str String to check -- @param #string value -- @return #bool True if str ends with value function string.endswith(str, value) return value == "" or str:sub(-#value) == value end --- Splits a string on a separator. For example: --- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"} -- @param #string input String to split -- @param #string separator What to split on -- @return #table individual strings function string.split(input, separator) local parts = {} for part in input:gmatch("[^" .. separator .. "]+") do table.insert(parts, part) end return parts end --- Checks if a string contains a substring. Easier to remember for Python people :) --- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"} -- @param #string str -- @param #string value -- @return #bool True if str contains value function string.contains(str, value) return string.match(str, value) end --- Given tbl is a indexed table ({"hello", "dcs", "world"}), checks if element exists in the table. --- The table can be made up out of complex tables or values as well -- @param #table tbl -- @param #string element -- @return #bool True if tbl contains element function table.contains(tbl, element) if element == nil or tbl == nil then return false end local index = 1 while tbl[index] do if tbl[index] == element then return true end index = index + 1 end return false end --- Checks if a table contains a specific key. -- @param #table tbl Table to check -- @param #string key Key to look for -- @return #bool True if tbl contains key function table.contains_key(tbl, key) if tbl[key] ~= nil then return true else return false end end --- Inserts a unique element into a table. -- @param #table tbl Table to insert into -- @param #string element Element to insert function table.insert_unique(tbl, element) if element == nil or tbl == nil then return end if not table.contains(tbl, element) then table.insert(tbl, element) end end --- Removes an element from a table by its value. -- @param #table tbl Table to remove from -- @param #string element Element to remove function table.remove_by_value(tbl, element) local indices_to_remove = {} local index = 1 for _, value in pairs(tbl) do if value == element then table.insert(indices_to_remove, index) end index = index + 1 end for _, idx in pairs(indices_to_remove) do table.remove(tbl, idx) end end --- Removes an element from a table by its key. -- @param #table table Table to remove from -- @param #string key Key of the element to remove -- @return #string Removed element function table.remove_key(table, key) local element = table[key] table[key] = nil return element end --- Finds the index of an element in a table. -- @param #table table Table to search -- @param #string element Element to find -- @return #int Index of the element, or nil if not found function table.index_of(table, element) for i, v in ipairs(table) do if v == element then return i end end return nil end --- Counts the number of elements in a table. -- @param #table T Table to count -- @return #int Number of elements in the table function table.length(T) local count = 0 for _ in pairs(T) do count = count + 1 end return count end --- Slices a table between two indices, much like Python's my_list[2:-1] -- @param #table tbl Table to slice -- @param #int first Starting index -- @param #int last Ending index -- @return #table Sliced table function table.slice(tbl, first, last) local sliced = {} local start = first or 1 local stop = last or table.length(tbl) local count = 1 for key, value in pairs(tbl) do if count >= start and count <= stop then sliced[key] = value end count = count + 1 end return sliced end --- Counts the number of occurrences of a value in a table. -- @param #table tbl Table to search -- @param #string value Value to count -- @return #int Number of occurrences of the value function table.count_value(tbl, value) local count = 0 for _, item in pairs(tbl) do if item == value then count = count + 1 end end return count end --- Add 2 table together, t2 gets added to t1 -- @param #table t1 First table -- @param #table t2 Second table -- @return #table Combined table function table.combine(t1, t2) if t1 == nil and t2 == nil then BASE:E("Both tables were empty!") end if t1 == nil then return t2 end if t2 == nil then return t1 end for i=1,#t2 do t1[#t1+1] = t2[i] end return t1 end --- Merges two tables into one. If a key exists in both t1 and t2, the value of t1 with be overwritten by the value of t2 -- @param #table t1 First table -- @param #table t2 Second table -- @return #table Merged table function table.merge(t1, t2) for k, v in pairs(t2) do if (type(v) == "table") and (type(t1[k] or false) == "table") then table.merge(t1[k], t2[k]) else t1[k] = v end end return t1 end --- Adds an item to the end of a table. -- @param #table tbl Table to add to -- @param #string item Item to add function table.add(tbl, item) tbl[#tbl + 1] = item end --- Shuffles the elements of a table. -- @param #table tbl Table to shuffle -- @return #table Shuffled table function table.shuffle(tbl) local new_table = {} for _, value in ipairs(tbl) do local pos = math.random(1, #new_table +1) table.insert(new_table, pos, value) end return new_table end --- Finds a key-value pair in a table. -- @param #table tbl Table to search -- @param #string key Key to find -- @param #string value Value to find -- @return #table Table containing the key-value pair, or nil if not found function table.find_key_value_pair(tbl, key, value) for k, v in pairs(tbl) do if type(v) == "table" then local result = table.find_key_value_pair(v, key, value) if result ~= nil then return result end elseif k == key and v == value then return tbl end end return nil end --- Convert a decimal to octal -- @param #number Number the number to convert -- @return #number Octal function UTILS.DecimalToOctal(Number) if Number < 8 then return Number end local number = tonumber(Number) local octal = "" local n=1 while number > 7 do local number1 = number%8 octal = string.format("%d",number1)..octal local number2 = math.abs(number/8) if number2 < 8 then octal = string.format("%d",number2)..octal end number = number2 n=n+1 end return tonumber(octal) end --- Convert an octal to decimal -- @param #number Number the number to convert -- @return #number Decimal function UTILS.OctalToDecimal(Number) return tonumber(Number,8) end