----------------------------------------------------------------------------------- -- Veaf scripts 5.103.3/debug;2025.12.16.12.16.24 ----------------------------------------------------------------------------------- ------------------ START script veaf.lua ------------------ ------------------------------------------------------------------ -- VEAF root script for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Contains all the constants and utility functions required by the other VEAF script libraries -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veaf = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the root VEAF constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veaf.Id = "VEAF" --- Version. veaf.Version = "1.56.2" --- Development version ? veaf.Development = true veaf.SecurityDisabled = false -- trace level, specific to this module --veaf.LogLevel = "debug" --veaf.LogLevel = "trace" veaf.ForcedLogLevel = "debug" -- log level, limiting all the modules veaf.BaseLogLevel = 5 --trace veaf.DEFAULT_GROUND_SPEED_KPH = 30 --- if true, the spawned group names will not contain any information pertaining to their type veaf.HideNamesFromSpawnedGroups = true ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veaf.config = {} veaf.triggerZones = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- veaf.theatreName = { Caucasus = "Caucasus", Nevada = "Nevada", Normandy = "Normandy", PersianGulf = "PersianGulf", TheChannel = "TheChannel", Syria = "Syria", MarianaIslands = "MarianaIslands", Falklands = "Falklands", Sinai = "SinaiMap", Kola = "Kola", Afghanistan = "Afghanistan", } veaf.ERA = { WW2 = "WW2", COLD_WAR = "COLD_WAR", MODERN = "MODERN", } veaf.config.era = veaf.ERA.MODERN -- default era ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- veaf.EVENTMETA = { [world.event.S_EVENT_SHOT] = { Order = 1, Side = "I", Event = "OnEventShot", Text = "S_EVENT_SHOT" }, [world.event.S_EVENT_HIT] = { Order = 1, Side = "T", Event = "OnEventHit", Text = "S_EVENT_HIT" }, [world.event.S_EVENT_TAKEOFF] = { Order = 1, Side = "I", Event = "OnEventTakeoff", Text = "S_EVENT_TAKEOFF" }, [world.event.S_EVENT_LAND] = { Order = 1, Side = "I", Event = "OnEventLand", Text = "S_EVENT_LAND" }, [world.event.S_EVENT_CRASH] = { Order = -1, Side = "I", Event = "OnEventCrash", Text = "S_EVENT_CRASH" }, [world.event.S_EVENT_EJECTION] = { Order = 1, Side = "I", Event = "OnEventEjection", Text = "S_EVENT_EJECTION" }, [world.event.S_EVENT_REFUELING] = { Order = 1, Side = "I", Event = "OnEventRefueling", Text = "S_EVENT_REFUELING" }, [world.event.S_EVENT_DEAD] = { Order = -1, Side = "I", Event = "OnEventDead", Text = "S_EVENT_DEAD" }, [world.event.S_EVENT_PILOT_DEAD] = { Order = 1, Side = "I", Event = "OnEventPilotDead", Text = "S_EVENT_PILOT_DEAD" }, [world.event.S_EVENT_BASE_CAPTURED] = { Order = 1, Side = "I", Event = "OnEventBaseCaptured", Text = "S_EVENT_BASE_CAPTURED" }, [world.event.S_EVENT_MISSION_START] = { Order = 1, Side = "N", Event = "OnEventMissionStart", Text = "S_EVENT_MISSION_START" }, [world.event.S_EVENT_MISSION_END] = { Order = 1, Side = "N", Event = "OnEventMissionEnd", Text = "S_EVENT_MISSION_END" }, [world.event.S_EVENT_TOOK_CONTROL] = { Order = 1, Side = "N", Event = "OnEventTookControl", Text = "S_EVENT_TOOK_CONTROL" }, [world.event.S_EVENT_REFUELING_STOP] = { Order = 1, Side = "I", Event = "OnEventRefuelingStop", Text = "S_EVENT_REFUELING_STOP" }, [world.event.S_EVENT_BIRTH] = { Order = 1, Side = "I", Event = "OnEventBirth", Text = "S_EVENT_BIRTH" }, [world.event.S_EVENT_HUMAN_FAILURE] = { Order = 1, Side = "I", Event = "OnEventHumanFailure", Text = "S_EVENT_HUMAN_FAILURE" }, [world.event.S_EVENT_ENGINE_STARTUP] = { Order = 1, Side = "I", Event = "OnEventEngineStartup", Text = "S_EVENT_ENGINE_STARTUP" }, [world.event.S_EVENT_ENGINE_SHUTDOWN] = { Order = 1, Side = "I", Event = "OnEventEngineShutdown", Text = "S_EVENT_ENGINE_SHUTDOWN" }, [world.event.S_EVENT_PLAYER_ENTER_UNIT] = { Order = 1, Side = "I", Event = "OnEventPlayerEnterUnit", Text = "S_EVENT_PLAYER_ENTER_UNIT" }, [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = { Order = -1, Side = "I", Event = "OnEventPlayerLeaveUnit", Text = "S_EVENT_PLAYER_LEAVE_UNIT" }, [world.event.S_EVENT_PLAYER_COMMENT] = { Order = 1, Side = "I", Event = "OnEventPlayerComment", Text = "S_EVENT_PLAYER_COMMENT" }, [world.event.S_EVENT_SHOOTING_START] = { Order = 1, Side = "I", Event = "OnEventShootingStart", Text = "S_EVENT_SHOOTING_START" }, [world.event.S_EVENT_SHOOTING_END] = { Order = 1, Side = "I", Event = "OnEventShootingEnd", Text = "S_EVENT_SHOOTING_END" }, [world.event.S_EVENT_MARK_ADDED] = { Order = 1, Side = "I", Event = "OnEventMarkAdded", Text = "S_EVENT_MARK_ADDED" }, [world.event.S_EVENT_MARK_CHANGE] = { Order = 1, Side = "I", Event = "OnEventMarkChange", Text = "S_EVENT_MARK_CHANGE" }, [world.event.S_EVENT_MARK_REMOVED] = { Order = 1, Side = "I", Event = "OnEventMarkRemoved", Text = "S_EVENT_MARK_REMOVED" } } --[[ json.lua Used from https://gist.github.com/tylerneylon/59f4bcf316be525b30ab with authorization A compact pure-Lua JSON library. The main functions are: json.stringify, json.parse. ## json.stringify: This expects the following to be true of any tables being encoded: * They only have string or number keys. Number keys must be represented as strings in json; this is part of the json spec. * They are not recursive. Such a structure cannot be specified in json. A Lua table is considered to be an array if and only if its set of keys is a consecutive sequence of positive integers starting at 1. Arrays are encoded like so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json object, encoded like so: `{"key1": 2, "key2": false}`. Because the Lua nil value cannot be a key, and as a table value is considerd equivalent to a missing key, there is no way to express the json "null" value in a Lua table. The only way this will output "null" is if your entire input obj is nil itself. An empty Lua table, {}, could be considered either a json object or array - it's an ambiguous edge case. We choose to treat this as an object as it is the more general type. To be clear, none of the above considerations is a limitation of this code. Rather, it is what we get when we completely observe the json specification for as arbitrary a Lua object as json is capable of expressing. ## json.parse: This function parses json, with the exception that it does not pay attention to \u-escaped unicode code points in strings. It is difficult for Lua to return null as a value. In order to prevent the loss of keys with a null value in a json string, this function uses the one-off table value json.null (which is just an empty table) to indicate null values. This way you can check if a value is null with the conditional `val == json.null`. If you have control over the data and are using Lua, I would recommend just avoiding null values in your data to begin with. --]] veaf.json = {} -- Internal functions. local function kind_of(obj) if type(obj) ~= 'table' then return type(obj) end local i = 1 for _ in pairs(obj) do if obj[i] ~= nil then i = i + 1 else return 'table' end end if i == 1 then return 'table' else return 'array' end end local function escape_str(s) local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} for i, c in ipairs(in_char) do s = s:gsub(c, '\\' .. out_char[i]) end return s end -- Returns pos, did_find; there are two cases: -- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. -- 2. Delimiter not found: pos = pos after leading space; did_find = false. -- This throws an error if err_if_missing is true and the delim is not found. local function skip_delim(str, pos, delim, err_if_missing) pos = pos + #str:match('^%s*', pos) if str:sub(pos, pos) ~= delim then if err_if_missing then error('Expected ' .. delim .. ' near position ' .. pos) end return pos, false end return pos + 1, true end -- Expects the given pos to be the first character after the opening quote. -- Returns val, pos; the returned pos is after the closing quote character. local function parse_str_val(str, pos, val) val = val or '' local early_end_error = 'End of input found while parsing string.' if pos > #str then error(early_end_error) end local c = str:sub(pos, pos) if c == '"' then return val, pos + 1 end if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end -- We must have a \ character. local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} local nextc = str:sub(pos + 1, pos + 1) if not nextc then error(early_end_error) end return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) end -- Returns val, pos; the returned pos is after the number's final character. local function parse_num_val(str, pos) local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) local val = tonumber(num_str) if not val then error('Error parsing number at position ' .. pos .. '.') end return val, pos + #num_str end -- Public values and functions. function veaf.json.stringify(obj, as_key) local s = {} -- We'll build the string as an array of strings to be concatenated. local kind = kind_of(obj) -- This is 'array' if it's an array or type(obj) otherwise. if kind == 'array' then if as_key then error('Can\'t encode array as key.') end s[#s + 1] = '[' for i, val in ipairs(obj) do if i > 1 then s[#s + 1] = ', ' end s[#s + 1] = veaf.json.stringify(val) end s[#s + 1] = ']' elseif kind == 'table' then if as_key then error('Can\'t encode table as key.') end s[#s + 1] = '{' for k, v in pairs(obj) do if #s > 1 then s[#s + 1] = ', ' end s[#s + 1] = veaf.json.stringify(k, true) s[#s + 1] = ':' s[#s + 1] = veaf.json.stringify(v) end s[#s + 1] = '}' elseif kind == 'string' then return '"' .. escape_str(obj) .. '"' elseif kind == 'number' then if as_key then return '"' .. tostring(obj) .. '"' end return tostring(obj) elseif kind == 'boolean' then return tostring(obj) elseif kind == 'nil' then return 'null' else return '"Unjsonifiable type: ' .. kind .. '."' --error('Unjsonifiable type: ' .. kind .. '.') end return table.concat(s) end veaf.json.null = {} -- This is a one-off table to represent the null value. function veaf.json.parse(str, pos, end_delim) pos = pos or 1 if pos > #str then error('Reached unexpected end of input.') end local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. local first = str:sub(pos, pos) if first == '{' then -- Parse an object. local obj, key, delim_found = {}, true, true pos = pos + 1 while true do -- not my code ! ---@diagnostic disable-next-line: cast-local-type key, pos = veaf.json.parse(str, pos, '}') if key == nil then return obj, pos end if not delim_found then error('Comma missing between object items.') end pos = skip_delim(str, pos, ':', true) -- true -> error if missing. -- not my code ! ---@diagnostic disable-next-line: need-check-nil obj[key], pos = veaf.json.parse(str, pos) pos, delim_found = skip_delim(str, pos, ',') end elseif first == '[' then -- Parse an array. local arr, val, delim_found = {}, true, true pos = pos + 1 while true do -- not my code ! ---@diagnostic disable-next-line: cast-local-type val, pos = veaf.json.parse(str, pos, ']') if val == nil then return arr, pos end if not delim_found then error('Comma missing between array items.') end arr[#arr + 1] = val pos, delim_found = skip_delim(str, pos, ',') end elseif first == '"' then -- Parse a string. return parse_str_val(str, pos + 1) elseif first == '-' or first:match('%d') then -- Parse a number. return parse_num_val(str, pos) elseif first == end_delim then -- End of an object or array. return nil, pos + 1 else -- Parse true, false, or null. local literals = {['true'] = true, ['false'] = false, ['null'] = veaf.json.null} for lit_str, lit_val in pairs(literals) do local lit_end = pos + #lit_str - 1 if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end end local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) error('Invalid json syntax starting at ' .. pos_info_str) end end local escapeChars = nil ---Escapes a string so it can no longer be a pattern (regex) ---@param stringToEscape string ---@return string function veaf.escapeRegex(stringToEscape) local regexCharsToEscape = "^$()%.[]*+-?" if not escapeChars then escapeChars = {} for i = 1, string.len(regexCharsToEscape) do local char = string.sub(regexCharsToEscape,i,i) escapeChars[char] = true end end local result = "" if stringToEscape then for i = 1, string.len(stringToEscape) do local char = string.sub(stringToEscape,i,i) if escapeChars[char] then result = result .. "%" end result = result .. char end end return result end --- efficiently remove elements from a table --- credit : Mitch McMabers (https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating) function veaf.arrayRemoveWhen(t, fnKeep) local pristine = true local j, n = 1, #t; for i=1,n do if (fnKeep(t, i, j)) then if (i ~= j) then -- Keep i's value, move it to j's pos. t[j] = t[i]; t[i] = nil; else -- Keep i's value, already at j's pos. end j = j + 1; else t[i] = nil; pristine = false end end return not pristine; end function veaf.vecToString(vec) local result = "" if vec.x then result = result .. string.format(" x=%.1f", vec.x) end if vec.y then result = result .. string.format(" y=%.1f", vec.y) end if vec.z then result = result .. string.format(" z=%.1f", vec.z) end return result end function veaf.discoverMetadata(o) local text = "" for key,value in pairs(getmetatable(o)) do text = text .. " - ".. key.."\n"; end return text end function veaf.serialize(name, value, level) -- mostly based on slMod serializer local function _basicSerialize(s) if s == nil then return "\"\"" else if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then return tostring(s) elseif type(s) == 'string' then return string.format('%q', s) end end end -----Based on ED's serialize_simple2 local basicSerialize = function(o) if type(o) == "number" then return tostring(o) elseif type(o) == "boolean" then return tostring(o) else -- assume it is a string return _basicSerialize(o) end end local function _sortNumberOrCaseInsensitive(a,b) if type(a) == "string" or type(b) == "string" then return string.lower(a) < string.lower(b) else return a < b end end local serialize_to_t = function(name, value, level) ----Based on ED's serialize_simple2 local var_str_tbl = {} if level == nil then level = "" end if level ~= "" then level = level .. " " end table.insert(var_str_tbl, level .. name .. " = ") if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then table.insert(var_str_tbl, basicSerialize(value) .. ",\n") elseif type(value) == "table" then table.insert(var_str_tbl, "{\n") local tkeys = {} -- populate the table that holds the keys for k in pairs(value) do table.insert(tkeys, k) end -- sort the keys table.sort(tkeys, _sortNumberOrCaseInsensitive) -- use the keys to retrieve the values in the sorted order for _, k in ipairs(tkeys) do -- serialize its fields local v = value[k] local key if type(k) == "number" then key = string.format("[%s]", k) else key = string.format("[%q]", k) end table.insert(var_str_tbl, veaf.serialize(key, v, level .. " ")) end if level == "" then table.insert(var_str_tbl, level .. "} -- end of " .. name .. "\n") else table.insert(var_str_tbl, level .. "}, -- end of " .. name .. "\n") end else veaf.loggers.get(veaf.Id):error("Cannot serialize a " .. type(value)) end return var_str_tbl end local t_str = serialize_to_t(name, value, level) return table.concat(t_str) end function veaf.ifnn(o, field) if o then if o[field] then if type(o[field]) == "function" then local sta, res = pcall(o[field],o) if sta then return res else return nil end else return o[field] end end else return nil end end function veaf.ifnns(o, fields) local result = nil if o then result = {} if type(fields) ~= "table" then local field = fields fields = { field } end for _, field in pairs(fields) do if o[field] then if type(o[field]) == "function" then local sta, res = pcall(o[field],o) if sta then result[field] = res else result[field] = nil end else result[field] = o[field] end end end end return result end function veaf.isNullOrEmpty(s) return (s == nil or (type(s) == "string" and s == "")) end function veaf.tableContains(table, element) if (table == nil or element == nil) then return false end for _, e in pairs(table) do if (e == element) then return true end end return false end function veaf.p(o, level, skip, includeMeta, dontRecurse) if o and type(o) == "table" and (o.x and o.z and o.y and #o == 3) then return string.format("{x=%s, z=%s, y=%s}", veaf.p(o.x), veaf.p(o.z), veaf.p(o.y)) elseif o and type(o) == "table" and (o.x and o.y and #o == 2) then return string.format("{x=%s, y=%s}", veaf.p(o.x), veaf.p(o.y)) end local skip = skip if skip and type(skip)=="table" then for _, value in ipairs(skip) do skip[value]=true end end return veaf._p(o, level, skip, includeMeta, dontRecurse) end function veaf._p(o, level, skip, includeMeta, dontRecurse) local MAX_LEVEL = 20 if level == nil then level = 0 end if level > MAX_LEVEL then veaf.loggers.get(veaf.Id):error("max depth reached in veaf.p : "..tostring(MAX_LEVEL)) return "" end local text = "" if o == nil then text = "[nil]" elseif (type(o) == "table") and not(dontRecurse) then text = "\n" local keys = {} local values = {} for key, value in pairs(o) do local sKey = tostring(key) table.insert(keys, sKey) values[sKey] = value end table.sort(keys) for _, key in pairs(keys) do local value = values[key] for i=0, level do text = text .. " " end if not (skip and skip[key]) then text = text .. ".".. key.."="..veaf.p(value, level+1, skip, includeMeta, dontRecurse) .. "\n" else text = text .. ".".. key.."= [[SKIPPED]]\n" end end if includeMeta then local metatable = getmetatable(o) if metatable then text = "\n" local keys = {} local values = {} for key, value in pairs(metatable) do local sKey = tostring(key) table.insert(keys, sKey) values[sKey] = value end table.sort(keys) for _, key in pairs(keys) do local value = values[key] for i=0, level do text = text .. " " end if not (skip and skip[key]) then if key == "getID" then value = o:getID() elseif key == "getName" then value = o:getName() elseif key == "getTypeName" then value = o:getTypeName() elseif key == "getDesc" then value = o:getDesc() end text = text .. "[META].".. key.."="..veaf.p(value, level+1, skip, includeMeta, true) .. "\n" else text = text .. "[META].".. key.."= [[SKIPPED]]\n" end end end end elseif (type(o) == "function") then text = "[function]" elseif (type(o) == "boolean") then if o == true then text = "[true]" else text = "[false]" end else text = tostring(o) end return text end function veaf.length(T) local count = 0 if T ~= nil then for _ in pairs(T) do count = count + 1 end end return count end --- Simple round function veaf.round(num, numDecimalPlaces) local mult = 10^(numDecimalPlaces or 0) return math.floor(num * mult + 0.5) / mult end --- shuffle a table elements around function veaf.shuffle(tbl) for i = #tbl, 2, -1 do local j = math.random(i) tbl[i], tbl[j] = tbl[j], tbl[i] end return tbl end --- Return the height of the land at the coordinate. function veaf.getLandHeight(vec3) veaf.loggers.get(veaf.Id):trace(string.format("getLandHeight: vec3 x=%.1f y=%.1f, z=%.1f", vec3.x, vec3.y, vec3.z)) local vec2 = {x = vec3.x, y = vec3.z} veaf.loggers.get(veaf.Id):trace(string.format("getLandHeight: vec2 x=%.1f z=%.1f", vec3.x, vec3.z)) -- We add 1 m "safety margin" because data from getlandheight gives the surface and wind at or below the surface is zero! local height = math.floor(land.getHeight(vec2) + 1) veaf.loggers.get(veaf.Id):trace(string.format("getLandHeight: result height=%.1f",height)) return height end function veaf.invertHeading(heading) veaf.loggers.get(veaf.Id):trace(string.format("invertHeading(%s)", veaf.p(heading))) local result = heading - 180 if result <= 0 then result = result + 360 end return result end -- get a LL position based on a string -- can be UTM (U38TMP334456 or u37TMP4351) -- can be LL with either : or - as a separator, and either DMS, DM decimal, or D decimal (N42:23:45E044-12.5 or N42.3345E044-12.5) function veaf.computeLLFromString(value) local function _computeLLValueFromString(value) local result = -1 if value:find(":") or value:find("-") then -- convert in arc-seconds local values = veaf.splitWithPattern(value, "[:-]+") local weights = {3600, 60, 1} for _, element in pairs(values) do veaf.loggers.get(veaf.Id):trace(string.format("element=%s",veaf.p(element))) local weight = table.remove(weights, 1) local elementInArcSec = tonumber(element)*weight result = result + elementInArcSec end return result / 3600 else -- decimals return tonumber(value) end end if value then local _value = value:lower() local _firstChar = _value:sub(1,1) if _firstChar == "u" then -- UTM coordinates local _zone, _digraph, _digits = _value:match("u(%d%d[a-z])([a-z][a-z])(%d+)") veaf.loggers.get(veaf.Id):trace(string.format("_zone=%s",veaf.p(_zone))) veaf.loggers.get(veaf.Id):trace(string.format("_digraph=%s",veaf.p(_digraph))) veaf.loggers.get(veaf.Id):trace(string.format("_digits=%s",veaf.p(_digits))) if _zone and _digraph and _digits then local _nDigits = #_digits local _northingString = _digits:sub(_nDigits/2+1) local _northing = tonumber(_northingString) veaf.loggers.get(veaf.Id):trace(string.format("_northing=%s",veaf.p(_northing))) if #_northingString == 1 then _northing = _northing * 10000 elseif #_northingString == 2 then _northing = _northing * 1000 elseif #_northingString == 3 then _northing = _northing * 100 elseif #_northingString == 4 then _northing = _northing * 10 end local _eastingString = _digits:sub(1, _nDigits/2) local _easting = tonumber(_eastingString) veaf.loggers.get(veaf.Id):trace(string.format("_easting=%s",veaf.p(_easting))) if #_eastingString == 1 then _easting = _easting * 10000 elseif #_eastingString == 2 then _easting = _easting * 1000 elseif #_eastingString == 3 then _easting = _easting * 100 elseif #_eastingString == 4 then _easting = _easting * 10 end local _utm= { UTMZone = _zone:upper(), MGRSDigraph = _digraph:upper(), Easting = _easting, Northing = _northing } veaf.loggers.get(veaf.Id):trace(string.format("_utm=%s",veaf.p(_utm))) return coord.MGRStoLL(_utm) end elseif _firstChar == "n" or _firstChar == "s" or _firstChar == "e" or _firstChar == "w" then -- LL coordinates local _signLat, _digitsLat, _signLon, _digitsLon = _value:match([[([news])([%d:\.-]+)([news])([%d:\.-]+)]]) if _digitsLat and _digitsLon then local _multLat = 1 if _signLat == "s" then _multLat = -1 end local _multLon = 1 if _signLon == "w" then _multLon = -1 end local _lat = _multLat * _computeLLValueFromString(_digitsLat) local _lon = _multLon * _computeLLValueFromString(_digitsLon) return _lat, _lon end end end -- unrecognized format return nil end function veaf.findDcsAirbase(name) local dcsAirbase = Airbase.getByName(name) if (dcsAirbase) then return dcsAirbase end -- Remove "AIRBASE " prefix if it exists (case insensitive) name = name:gsub("^[Aa][Ii][Rr][Bb][Aa][Ss][Ee]%s+", "") -- Helper function to normalize strings local function normalize(s) -- Convert to lowercase s = s:lower() -- Remove spaces and punctuation s = s:gsub("[%s%p]", "") return s end name = normalize(name) local airBases = world.getAirbases() for i = 1, #airBases do dcsAirbase = airBases[i] local sAirbaseName = dcsAirbase:getName() -- Normalize each list item sAirbaseName = normalize(sAirbaseName) -- Compare normalized strings if (sAirbaseName == name) then return dcsAirbase end end return nil end function veaf.silenceAtcOnAllAirbases() local bases = world.getAirbases() for _, base in pairs(bases) do if base:getDesc() then if base:getDesc().category == Airbase.Category.AIRDROME then veaf.loggers.get(veaf.Id):info("silencing ATC at base %s", veaf.p(base:getDesc().displayName)) base:setRadioSilentMode(true) end end end end --- Return a point at the same coordinates, but on the surface function veaf.placePointOnLand(vec3) -- convert a vec2 to a vec3 if not vec3.z then vec3.z = vec3.y vec3.y = 0 end if not vec3.y then vec3.y = 0 end veaf.loggers.get(veaf.Id):trace(string.format("getLandHeight: vec3 x=%.1f y=%.1f, z=%.1f", vec3.x, vec3.y, vec3.z)) local height = veaf.getLandHeight(vec3) veaf.loggers.get(veaf.Id):trace(string.format("getLandHeight: result height=%.1f",height)) local result={x=vec3.x, y=height, z=vec3.z} veaf.loggers.get(veaf.Id):trace(string.format("placePointOnLand: result x=%.1f y=%.1f, z=%.1f", result.x, result.y, result.z)) return result end --- Trim a string function veaf.trim(s) local a = s:match('^%s*()') local b = s:match('()%s*$', a) return s:sub(a,b-1) end --- Split string. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua function veaf.splitWithPattern(str, pat) local t = {} -- NOTE: use {n = 0} in Lua-5.0 local fpat = "(.-)" .. pat local last_end = 1 local s, e, cap = str:find(fpat, 1) while s do if s ~= 1 or cap ~= "" then table.insert(t, cap) end last_end = e+1 s, e, cap = str:find(fpat, last_end) end if last_end <= #str then cap = str:sub(last_end) table.insert(t, cap) end return t end function veaf.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 --- Break string around a separator function veaf.breakString(str, sep) local regex = ("^([^%s]+)%s(.*)$"):format(sep, sep) local a, b = str:match(regex) if not a then a = str end local result = {a, b} return result end --- Get the average center of a group position (average point of all units position) function veaf.getAveragePosition(group) if type(group) == "string" then group = Group.getByName(group) end local count local totalPosition = {x = 0,y = 0,z = 0} if group then local units = Group.getUnits(group) for count = 1,#units do if units[count] then totalPosition = mist.vec.add(totalPosition,Unit.getPosition(units[count]).p) end end if #units > 0 then return mist.vec.scalar_mult(totalPosition,1/#units) else return nil end else return nil end end function veaf.emptyFunction() end function veaf.getMagneticDeclination() local nDeclination = 0 local sTheatre = string.lower(env.mission.theatre) if (sTheatre == "caucasus") then nDeclination = 6 elseif (sTheatre == "persiangulf") then nDeclination = 2 elseif (sTheatre == "nevada") then nDeclination = 12 elseif (sTheatre == "normandy") then nDeclination = -10 elseif (sTheatre == "thechannel") then nDeclination = -10 elseif (sTheatre == "syria") then nDeclination = 5 elseif (sTheatre == "marianaislands") then nDeclination = 2 elseif (sTheatre == "falklands") then nDeclination = 12 elseif (sTheatre == "sinaimap") then nDeclination = 4.8 elseif (sTheatre == "kola") then nDeclination = 15 elseif (sTheatre == "afghanistan") then nDeclination = 3 end return nDeclination end --- Returns the wind direction (from) and strength. function veaf.getWind(point) -- Get wind velocity vector. local windvec3 = atmosphere.getWind(point) local direction = math.floor(math.deg(math.atan2(windvec3.z, windvec3.x))) if direction < 0 then direction = direction + 360 end -- Convert TO direction to FROM direction. if direction > 180 then direction = direction-180 else direction = direction+180 end -- Calc 2D strength. local strength=math.floor(math.sqrt((windvec3.x)^2+(windvec3.z)^2)) -- Debug output. veaf.loggers.get(veaf.Id):trace(string.format("Wind data: point x=%.1f y=%.1f, z=%.1f", point.x, point.y,point.z)) veaf.loggers.get(veaf.Id):trace(string.format("Wind data: wind x=%.1f y=%.1f, z=%.1f", windvec3.x, windvec3.y,windvec3.z)) veaf.loggers.get(veaf.Id):trace(string.format("Wind data: |v| = %.1f", strength)) veaf.loggers.get(veaf.Id):trace(string.format("Wind data: ang = %.1f", direction)) -- Return wind direction and strength (in m/s). return direction, strength, windvec3 end ---comment ---@param mach number the mach number ---@param altitude any in feet, defaults to 10000 ---@param temperature any in celsius, defaults to ISA temperature at altitude function veaf.convertMachSpeed(mach, altitude, temperature) return veaf.convertSpeeds(mach, nil, nil, altitude, temperature) end ---comment ---@param ktas number the true airspeed in knots ---@param altitude any in feet, defaults to 10000 ---@param temperature any in celsius, defaults to ISA temperature at altitude function veaf.convertTrueAirSpeed(ktas, altitude, temperature) return veaf.convertSpeeds(nil, nil, ktas, altitude, temperature) end ---comment ---@param kias number the indicated airspeed in knots ---@param altitude any in feet, defaults to 10000 ---@param temperature any in celsius, defaults to ISA temperature at altitude function veaf.convertIndicatedAirSpeed(kias, altitude, temperature) return veaf.convertSpeeds(nil, kias, nil, altitude, temperature) end ---Computes speeds based on a speed parameter (mach, tas, ias) and altitude/temperature ---@param mach number? the mach number ---@param kias number? the indicated airspeed in knots ---@param ktas number? the true airspeed in knots ---@param altitude any in meters, defaults to 10000 ---@param temperature any in celsius, defaults to ISA temperature at altitude ---@param pressure any in pa, defaults to ISA temperature at altitude ---@return table result containing KTAS, KIAS, Mach, IAS_ms and TAS_ms function veaf.convertSpeeds(mach, kias, ktas, altitude, temperature, pressure) veaf.loggers.get(veaf.Id):debug("veaf.convertSpeeds(mach=%s, kias=%s, ktas=%s, altitude=%s, temperature=%s, pressure=%s) -> initial", veaf.p(mach), veaf.p(kias), veaf.p(ktas), veaf.p(altitude), veaf.p(temperature), veaf.p(pressure)) local result = { KTAS = 0, KIAS = 0, Mach = 0, } local h_tropopause = 11000 --m, tropopause start altitude< local altitude = altitude if not altitude then altitude = 10000 -- default to 10000m end local T0 = 288.15 --K, ISA+0 altitude, may need to be corrected for mission ground temp local Tz = -0.0065 --K/m, ISA temperature gradient in troposphere local T_tropopause = 216.65 --K, temperature at the border between tropopause and troposphere (temperature in the tropopause) local P0 = 101325 --Pa, standard pressure local Gamma = 1.4 --Air heat capacity ratio local r = 287.03 --J/kg/K Perfect Gas constant for air local g = 9.81 --m/s^2 gravity constant on earth, might need to account for which planet ED is on local temperature = temperature if not temperature then -- compute ISA temperature based on altitude if altitude1) (NOTE : (Pt-Ps)=deltaP) ---@return number returns the ratio deltaP/P (DPP) after the normal shock (what a pitot tube would measure for M>1) local function lord_rayleighDPP(mach) local A = ((Gamma+1)*mach^2/2)^B local C = ((Gamma+1)/(2*Gamma*mach^2-Gamma+1))^(B/Gamma); return A*C-1; end ---comment ---@param mach1 number the starting mach (mach_0 or mach_p) which determines the deltaP/P1 being computed (for a pitot tube at sea level, subscript 0 (IAS) or at altitude (TAS), subscript p) ---@param getTAS boolean? if true, switches to conversion mode from IAS to TAS ---@return number so if you provide only mach_P (TAS), this will return mach_0 (IAS), and if you provide mach_0 and getTAS true (IAS), this will return mach_P (TAS) local function getConvertedMach(mach1, getTAS) veaf.loggers.get(veaf.Id):debug("getConvertedMach(mach1 = %s, getTAS = %s", veaf.p(mach1), veaf.p(getTAS)) local DPP1 = 0; if mach1 > 1 then DPP1 = lord_rayleighDPP(mach1); --At this point it's still deltaP / Pp (DPPP) (subscript p = at pitot tube, subscript 0 = at sea level) else DPP1 = isentropicDPP(mach1); --At this point it's still deltaP / Pp (DPPP) (subscript p = at pitot tube, subscript 0 = at sea level) end veaf.loggers.get(veaf.Id):debug("DPP1 = %s -> initial", veaf.p(DPP1)) if getTAS then DPP1 = P0*DPP1/pressure --conversion from DPP0 to DPPP else DPP1 = pressure*DPP1/P0 --conversion from DPPP to DPP0 end veaf.loggers.get(veaf.Id):debug("DPP1 = %s -> final", veaf.p(DPP1)) local mach2 = 1 local function converge_2_DPP(machStep) while(lord_rayleighDPP(mach2) < DPP1) do --DPP2 = lord_rayleighDPP(mach2) mach2 = mach2+machStep end return mach2 end if DPP1 > lord_rayleighDPP(1) then mach2 = converge_2_DPP(0.25) - 0.25 --coarse veaf.loggers.get(veaf.Id):debug("coarse mach2 = %s", veaf.p(mach2)) mach2 = converge_2_DPP(0.0125) - 0.0125 --medium veaf.loggers.get(veaf.Id):debug("medium mach2 = %s", veaf.p(mach2)) mach2 = converge_2_DPP(0.00625) --fine veaf.loggers.get(veaf.Id):debug("fine mach2 = %s", veaf.p(mach2)) else mach2 = math.sqrt(2*((DPP1+1)^(1/B)-1)/(Gamma-1)) veaf.loggers.get(veaf.Id):debug("subsonic mach2 = %s", veaf.p(mach2)) end return mach2 end local ms_2_kt = 1.94384 local a1 = speedOfSound(temperature) local a0 = speedOfSound(T0) veaf.loggers.get(veaf.Id):debug("a0 = %s, a1 = %s", veaf.p(a0), veaf.p(a1)) if mach then -- compute speeds from mach number result.Mach = mach result.TAS_ms = mach * a1 result.KTAS = result.TAS_ms * ms_2_kt result.IAS_ms = getConvertedMach(result.Mach)*a0 result.KIAS = result.IAS_ms * ms_2_kt elseif kias then -- compute speeds from ias result.KIAS = kias result.IAS_ms = result.KIAS / ms_2_kt result.TAS_ms = getConvertedMach(result.IAS_ms/a0, true)*a1 result.KTAS = result.TAS_ms * ms_2_kt result.Mach = result.TAS_ms / a1 elseif ktas then -- compute speeds from tas result.KTAS = ktas result.TAS_ms = result.KTAS / ms_2_kt result.Mach = result.TAS_ms / a1 result.IAS_ms = getConvertedMach(result.Mach)*a0 result.KIAS = result.IAS_ms * ms_2_kt end veaf.loggers.get(veaf.Id):debug("veaf.convertSpeeds(mach=%s, kias=%s, ktas=%s, altitude=%s, temperature=%s, pressure=%s) -> final", veaf.p(result.Mach), veaf.p(result.KIAS), veaf.p(result.KTAS), veaf.p(altitude), veaf.p(temperature), veaf.p(pressure)) return result end --- Find a suitable point for spawning a unit in a -sized circle around a spot function veaf.findPointInZone(spawnSpot, dispersion, isShip) local unitPosition local tryCounter = 1000 local dispersion = dispersion or 0 local _dispersion = dispersion repeat -- Place the unit in a "dispersion" ft radius circle from the spawn spot unitPosition = mist.getRandPointInCircle(spawnSpot, _dispersion) local landType = land.getSurfaceType(unitPosition) tryCounter = tryCounter - 1 _dispersion = _dispersion + dispersion until ((isShip and landType == land.SurfaceType.WATER) or (not(isShip) and (landType == land.SurfaceType.LAND or landType == land.SurfaceType.ROAD or landType == land.SurfaceType.RUNWAY))) or tryCounter == 0 if tryCounter == 0 then return nil else return unitPosition end end ---Fixes a table of mixed units and unit names and returns a table of DCS units ---@param unitsOrNames table a list of units, unit names, or a mix ---@return table the DCS units function veaf.fixUnitsTable(unitsOrNames) local units = {} for _, unitOrName in pairs(unitsOrNames) do local unit = nil if type(unitOrName) == "table" then -- already an unit unit = unitOrName elseif type(unitOrName) == "string" then -- find by name unit = Unit.getByName(unitOrName) or StaticObject.getByName(unitOrName) end if unit then table.insert(units, unit) end end return units end ---checks if a unit is in a trigger zone ---@param unitOrName any a DCS unit or an unit name ---@param zoneOrName any a DCS trigger zone or a trigger zone name (any type) ---@return boolean true if the unit is in the trigger zone function veaf.isUnitInZone(unitOrName, zoneOrName) local unitIsInZone = false local unit = nil if unitOrName then if type(unitOrName) == "table" then -- already an unit unit = unitOrName elseif type(unitOrName) == "string" then -- find by name unit = Unit.getByName(unitOrName) or StaticObject.getByName(unitOrName) end end local zone = nil if zoneOrName then if type(zoneOrName) == "table" then -- already a DCS zone zone = zoneOrName elseif type(zoneOrName) == "string" then -- find by name zone = veaf.getTriggerZone(zoneOrName) end end if zone and unit then local unitPosition = unit:getPosition().p local objectCategory = Object.getCategory(unit) if unitPosition and ((objectCategory == 1 and unit:isActive() == true) or objectCategory ~= 1) then -- it is a unit and is active or it is not a unit if zone.verticies then local pointInPolygon = mist.pointInPolygon(unitPosition, zone.verticies) if pointInPolygon then unitIsInZone = true end else if ((unitPosition.x - zone.x)^2 + (unitPosition.z - zone.y)^2)^0.5 <= zone.radius then unitIsInZone = true end end end end return unitIsInZone end --- TODO doc function veaf.generateVehiclesRoute(startPoint, destination, onRoad, speed, patrol, groupName) veaf.loggers.get(veaf.Id):trace(string.format("veaf.generateVehiclesRoute(onRoad=[%s], speed=[%s], patrol=[%s])", tostring(onRoad or ""), tostring(speed or ""), tostring(patrol or ""))) speed = speed or veaf.DEFAULT_GROUND_SPEED_KPH onRoad = onRoad or false patrol = patrol or false veaf.loggers.get(veaf.Id):trace(string.format("startPoint = {x = %d, y = %d, z = %d}", startPoint.x, startPoint.y, startPoint.z)) local action = "Diamond" if onRoad then action = "On Road" end local endPoint = veafNamedPoints.getPoint(destination) if not(endPoint) then -- check if these are coordinates local _lat, _lon = veaf.computeLLFromString(destination) veaf.loggers.get(veaf.Id):trace(string.format("_lat=%s",veaf.p(_lat))) veaf.loggers.get(veaf.Id):trace(string.format("_lon=%s",veaf.p(_lon))) if _lat and _lon then endPoint = coord.LLtoLO(_lat, _lon) end end if not(endPoint) then local msg = "A point named "..destination.." cannot be found, and these are not valid coordinates !" veaf.loggers.get(veaf.Id):warn(msg) trigger.action.outText(msg, 5) return end veaf.loggers.get(veaf.Id):trace(string.format("endPoint=%s", veaf.p(endPoint))) local road_x = nil local road_z = nil local trueStartPoint = mist.utils.deepCopy(startPoint) if onRoad then veaf.loggers.get(veaf.Id):trace("setting startPoint on a road") road_x, road_z = land.getClosestPointOnRoads('roads',startPoint.x, startPoint.z) startPoint = veaf.placePointOnLand({x = road_x, y = 0, z = road_z}) else startPoint = veaf.placePointOnLand({x = startPoint.x, y = 0, z = startPoint.z}) end veaf.loggers.get(veaf.Id):trace(string.format("startPoint = {x = %d, y = %d, z = %d}", startPoint.x, startPoint.y, startPoint.z)) local trueEndPoint = mist.utils.deepCopy(endPoint) if onRoad then veaf.loggers.get(veaf.Id):trace("setting endPoint on a road") road_x, road_z =land.getClosestPointOnRoads('roads',endPoint.x, endPoint.z) endPoint = veaf.placePointOnLand({x = road_x, y = 0, z = road_z}) else endPoint = veaf.placePointOnLand({x = endPoint.x, y = 0, z = endPoint.z}) end veaf.loggers.get(veaf.Id):trace(string.format("endPoint = {x = %d, y = %d, z = %d}", endPoint.x, endPoint.y, endPoint.z)) local vehiclesRoute = { [1] = { ["x"] = trueStartPoint.x, ["y"] = trueStartPoint.z, ["alt"] = trueStartPoint.y, ["type"] = "Turning Point", ["ETA"] = 0, ["alt_type"] = "BARO", ["formation_template"] = "", ["name"] = "T_STA", ["ETA_locked"] = false, ["speed"] = 0, ["action"] = "Off Road", ["speed_locked"] = true, }, -- end of [1] [2] = { ["x"] = startPoint.x, ["y"] = startPoint.z, ["alt"] = startPoint.y, ["type"] = "Turning Point", ["ETA"] = 1, ["alt_type"] = "BARO", ["formation_template"] = "", ["name"] = "STA", ["ETA_locked"] = false, ["speed"] = speed / 3.6, ["action"] = action, ["speed_locked"] = false, }, -- end of [2] [3] = { ["x"] = endPoint.x, ["y"] = endPoint.z, ["alt"] = endPoint.y, ["type"] = "Turning Point", ["ETA"] = 2, ["alt_type"] = "BARO", ["formation_template"] = "", ["name"] = "END", ["ETA_locked"] = false, ["speed"] = speed / 3.6, ["action"] = action, ["speed_locked"] = true, }, -- end of [3] } if patrol then vehiclesRoute[4] = { ["x"] = startPoint.x, ["y"] = startPoint.z, ["alt"] = startPoint.y, ["type"] = "Turning Point", ["ETA"] = 3, ["alt_type"] = "BARO", ["formation_template"] = "", ["name"] = "STA2", ["ETA_locked"] = false, ["speed"] = speed / 3.6, ["action"] = action, ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { --sounds good ! doesn't work, pathfinding goes dumb if done this way --[1] = --{ -- ["enabled"] = true, -- ["auto"] = false, -- ["id"] = "GoToWaypoint", -- ["number"] = 1, -- ["params"] = -- { -- ["fromWaypointIndex"] = 4, -- ["nWaypointIndx"] = 2, -- }, -- end of ["params"] --}, -- end of [1] }, -- end of ["tasks"] }, -- end of ["params"] }, -- end of ["task"] ["speed_locked"] = true, } veaf.PatrolWatchdog(groupName, vehiclesRoute, speed/3.6, "notSeen") elseif onRoad then vehiclesRoute[4] = { ["x"] = trueEndPoint.x, ["y"] = trueEndPoint.z, ["alt"] = trueEndPoint.y, ["type"] = "Turning Point", ["ETA"] = 4, ["alt_type"] = "BARO", ["formation_template"] = "", ["name"] = "T_END", ["ETA_locked"] = false, ["speed"] = speed / 3.6, ["action"] = "Diamond", ["speed_locked"] = true, } end if not patrol then local endWaypoint = vehiclesRoute[4] if not onRoad then endWaypoint = vehiclesRoute[3] end endWaypoint.task = {} endWaypoint.task = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { [1] = { ["number"] = 1, ["auto"] = false, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = { ["action"] = { ["id"] = "Option", ["params"] = { ["value"] = 2, --Alarm State RED ["name"] = 9, --Alarm State }, -- end of ["params"] }, -- end of ["action"] }, -- end of ["params"] }, -- end of [1] }, -- end of ["tasks"] }, -- end of ["params"] } end veaf.loggers.get(veaf.Id):trace(string.format("vehiclesRoute = %s", veaf.p(vehiclesRoute))) return vehiclesRoute end function veaf.PatrolWatchdog(groupName,patrolRoute,speed,firstPass) veaf.loggers.get(veaf.Id):debug(string.format("veaf.PatrolWatchdog(groupName=%s, speed=%s, firstPass=%s)", veaf.p(groupName), veaf.p(speed), veaf.p(firstPass))) veaf.loggers.get(veaf.Id):trace(string.format("patrolRoute=%s", veaf.p(patrolRoute))) local rescheduleTime = 30 local maxDist = 10 if firstPass then maxDist = 200 end local startPoint = {x = patrolRoute[1].x, z = patrolRoute[1].y} local group = Group.getByName(groupName) if group then local controller = group:getController() if controller then veaf.loggers.get(veaf.Id):info("Checking if patrol is within " .. maxDist .. "m of it's start point...") local groupUnits = group:getUnits() if groupUnits and groupUnits[1] and groupUnits[1]:isActive() then local leadPos = groupUnits[1]:getPosition().p veaf.loggers.get(veaf.Id):trace(string.format("Lead vehicule name : %s", veaf.p(groupUnits[1]:getName()))) veaf.loggers.get(veaf.Id):trace(string.format("Lead vehicule position : %s", veaf.p(leadPos))) if leadPos then local distanceToStart = (leadPos.x-startPoint.x)^2+(leadPos.z-startPoint.z)^2 local result = distanceToStart < maxDist^2 if firstPass == "notSeen" and result then firstPass = "seenOnce" elseif firstPass == "seenOnce" and not result then firstPass = false end if not firstPass and result then veaf.loggers.get(veaf.Id):info("Lead vehicle in range, setting route !") mist.goRoute(group,patrolRoute) controller:setSpeed(speed) firstPass = "notSeen" elseif firstPass then veaf.loggers.get(veaf.Id):debug("Lead vehicle is passing in the bubble, rescheduling in " .. rescheduleTime .. "s !") else veaf.loggers.get(veaf.Id):debug("Lead vehicle/lead controller not found or lead vehicle not within " .. maxDist .. "m, rescheduling in " .. rescheduleTime .. "s !") end mist.scheduleFunction(veaf.PatrolWatchdog,{groupName, patrolRoute, speed, firstPass}, timer.getTime()+rescheduleTime) end elseif not groupUnits[1]:isActive() then veaf.loggers.get(veaf.Id):debug("Lead vehicle not active, rescheduling in 60s !") mist.scheduleFunction(veaf.PatrolWatchdog,{groupName, patrolRoute, speed, firstPass}, timer.getTime()+60) end end end veaf.loggers.get(veaf.Id):debug("========================================================================") end --- Add a unit to the on a suitable point in a -sized circle around a spot function veaf.addUnit(group, spawnSpot, dispersion, unitType, unitName, skill) local unitPosition = veaf.findPointInZone(spawnSpot, dispersion, false) if unitPosition ~= nil then table.insert( group, { ["x"] = unitPosition.x, ["y"] = unitPosition.y, ["type"] = unitType, ["name"] = unitName, ["heading"] = 0, ["skill"] = skill } ) else veaf.loggers.get(veaf.Id):info("cannot find a suitable position for unit "..unitType) end end --- Makes a group move to a waypoint set at a specific heading and at a distance covered at a specific speed in an hour function veaf.moveGroupAt(groupName, leadUnitName, heading, speed, timeInSeconds, endPosition, pMiddlePointDistance) veaf.loggers.get(veaf.Id):debug("veaf.moveGroupAt(groupName=" .. groupName .. ", heading="..heading.. ", speed=".. speed..", timeInSeconds="..(timeInSeconds or 0)) local unitGroup = Group.getByName(groupName) if unitGroup == nil then veaf.loggers.get(veaf.Id):error("veaf.moveGroupAt: " .. groupName .. ' not found') return false end local leadUnit = unitGroup:getUnits()[1] if leadUnitName then leadUnit = Unit.getByName(leadUnitName) end if leadUnit == nil then veaf.loggers.get(veaf.Id):error("veaf.moveGroupAt: " .. leadUnitName .. ' not found') return false end local headingRad = mist.utils.toRadian(heading) veaf.loggers.get(veaf.Id):trace("headingRad="..headingRad) local fromPosition = leadUnit:getPosition().p fromPosition = { x = fromPosition.x, y = fromPosition.z } veaf.loggers.get(veaf.Id):trace("fromPosition="..veaf.vecToString(fromPosition)) local mission = { id = 'Mission', params = { ["communication"] = true, ["start_time"] = 0, route = { points = { -- first point [1] = { --["alt"] = 0, ["type"] = "Turning Point", --["formation_template"] = "Diamond", --["alt_type"] = "BARO", ["x"] = fromPosition.x, ["y"] = fromPosition.z, ["name"] = "Starting position", ["action"] = "Turning Point", ["speed"] = 9999, -- ahead flank ["speed_locked"] = true, }, -- end of [1] }, } } } if pMiddlePointDistance then -- middle point (helps with having a more exact final bearing, specially with big hunks of steel like carriers) local middlePointDistance = 2000 if pMiddlePointDistance then middlePointDistance = pMiddlePointDistance end local newWaypoint1 = { x = fromPosition.x + middlePointDistance * math.cos(headingRad), y = fromPosition.y + middlePointDistance * math.sin(headingRad), } fromPosition.x = newWaypoint1.x fromPosition.y = newWaypoint1.y veaf.loggers.get(veaf.Id):trace("newWaypoint1="..veaf.vecToString(newWaypoint1)) table.insert(mission.params.route.points, { --["alt"] = 0, ["type"] = "Turning Point", --["formation_template"] = "Diamond", --["alt_type"] = "BARO", ["x"] = newWaypoint1.x, ["y"] = newWaypoint1.y, ["name"] = "Middle point", ["action"] = "Turning Point", ["speed"] = 9999, -- ahead flank ["speed_locked"] = true, } ) end local length if timeInSeconds then length = speed * timeInSeconds else length = speed * 3600 -- m travelled in 1 hour end veaf.loggers.get(veaf.Id):trace("length="..length .. " m") -- new route point local newWaypoint2 = { x = fromPosition.x + length * math.cos(headingRad), y = fromPosition.y + length * math.sin(headingRad), } veaf.loggers.get(veaf.Id):trace("newWaypoint2="..veaf.vecToString(newWaypoint2)) table.insert(mission.params.route.points, { --["alt"] = 0, ["type"] = "Turning Point", --["formation_template"] = "Diamond", --["alt_type"] = "BARO", ["x"] = newWaypoint2.x, ["y"] = newWaypoint2.y, ["name"] = "", ["action"] = "Turning Point", ["speed"] = speed, ["speed_locked"] = true, } ) if endPosition then table.insert(mission.params.route.points, { --["alt"] = 0, ["type"] = "Turning Point", --["formation_template"] = "Diamond", --["alt_type"] = "BARO", ["x"] = endPosition.x, ["y"] = endPosition.z, ["name"] = "Back to starting position", ["action"] = "Turning Point", ["speed"] = 9999, -- ahead flank ["speed_locked"] = true, } ) end -- replace whole mission unitGroup:getController():setTask(mission) return true end veaf.defaultAlarmState = 2 function veaf.readyForCombat(group, alarm, disperseTime) veaf.loggers.get(veaf.Id):trace(string.format("group=%s, alarm=%s, disperseTime=%s", veaf.p(group), veaf.p(alarm), veaf.p(disperseTime))) if type(group) == 'string' then group = Group.getByName(group) end if group then veaf.loggers.get(veaf.Id):trace("got group") local alarm = alarm if not alarm or alarm < 0 or alarm > 2 then alarm = veaf.defaultAlarmState end local disperseTime = disperseTime if not disperseTime or disperseTime < 0 then disperseTime = 0 end local cont = group:getController() cont:setOnOff(true) cont:setOption(AI.Option.Ground.id.ALARM_STATE, alarm) cont:setOption(AI.Option.Ground.id.DISPERSE_ON_ATTACK, disperseTime) -- set disperse on attack according to the option cont:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) -- set fire at will cont:setOption(AI.Option.Ground.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) -- set fire at will cont:setOption(AI.Option.Naval.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) -- set fire at will cont:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, true) -- engage air-to-ground weapons with SAMs end end -- Makes a group move to a specific waypoint at a specific speed function veaf.moveGroupTo(groupName, pos, speed, altitude) if not(altitude) then altitude = 0 end veaf.loggers.get(veaf.Id):debug("veaf.moveGroupTo(groupName=" .. groupName .. ", speed=".. speed .. ", altitude=".. altitude) veaf.loggers.get(veaf.Id):debug("pos="..veaf.vecToString(pos)) local unitGroup = Group.getByName(groupName) if unitGroup == nil then veaf.loggers.get(veaf.Id):error("veaf.moveGroupTo: " .. groupName .. ' not found') return false end local route = { [1] = { ["alt"] = altitude, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = veaf.round(speed, 2), ["type"] = "Turning Point", ["x"] = pos.x, ["y"] = pos.z, ["speed_locked"] = true, }, [2] = { ["alt"] = altitude, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 0, ["type"] = "Turning Point", ["x"] = pos.x, ["y"] = pos.z, ["speed_locked"] = true, }, } -- order group to new waypoint mist.goRoute(groupName, route) return true end function veaf.getAvgGroupPos(groupName) -- stolen from Mist and corrected local group = groupName -- sometimes this parameter is actually a group if type(groupName) == 'string' and Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then group = Group.getByName(groupName) end local units = {} for i = 1, group:getSize() do table.insert(units, group:getUnit(i):getName()) end return mist.getAvgPos(units) end --- Computes the coordinates of a point offset from a route of a certain distance, at a certain distance from route start --- e.g. we go from [startingPoint] to [destinationPoint], and at [distanceFromStartingPoint] we look at [offset] meters (left if <0, right else) function veaf.computeCoordinatesOffsetFromRoute(startingPoint, destinationPoint, distanceFromStartingPoint, offset) veaf.loggers.get(veaf.Id):trace("startingPoint="..veaf.vecToString(startingPoint)) veaf.loggers.get(veaf.Id):trace("destinationPoint="..veaf.vecToString(destinationPoint)) local vecAB = {x = destinationPoint.x +- startingPoint.x, y = destinationPoint.y - startingPoint.y, z = destinationPoint.z - startingPoint.z} veaf.loggers.get(veaf.Id):trace("vecAB="..veaf.vecToString(vecAB)) local alpha = math.atan2(vecAB.x, vecAB.z) -- atan2(y, x) veaf.loggers.get(veaf.Id):trace("alpha="..alpha) local r = math.sqrt(distanceFromStartingPoint * distanceFromStartingPoint + offset * offset) veaf.loggers.get(veaf.Id):trace("r="..r) local beta = math.atan(offset / distanceFromStartingPoint) veaf.loggers.get(veaf.Id):trace("beta="..beta) local tho = alpha + beta veaf.loggers.get(veaf.Id):trace("tho="..tho) local offsetPoint = { z = r * math.cos(tho) + startingPoint.z, y = 0, x = r * math.sin(tho) + startingPoint.x} veaf.loggers.get(veaf.Id):trace("offsetPoint="..veaf.vecToString(offsetPoint)) local offsetPointOnLand = veaf.placePointOnLand(offsetPoint) veaf.loggers.get(veaf.Id):trace("offsetPointOnLand="..veaf.vecToString(offsetPointOnLand)) return offsetPointOnLand, offsetPoint end function veaf.getBearingAndRangeFromTo(fromPoint, toPoint) veaf.loggers.get(veaf.Id):trace("fromPoint="..veaf.vecToString(fromPoint)) veaf.loggers.get(veaf.Id):trace("toPoint="..veaf.vecToString(toPoint)) local vec = { z = toPoint.z - fromPoint.z, x = toPoint.x - fromPoint.x} local angle = mist.utils.round(mist.utils.toDegree(mist.utils.getDir(vec)), 0) local distance = mist.utils.get2DDist(toPoint, fromPoint) return angle, distance, mist.utils.round(distance / 1000, 0), mist.utils.round(mist.utils.metersToNM(distance), 0) end function veaf.getGroupsOfCoalition(coa) local coalitions = { coalition.side.RED, coalition.side.BLUE, coalition.side.NEUTRAL} if coa then coalitions = { coa } end local allDcsGroups = {} for _, coa in pairs(coalitions) do local dcsGroups = coalition.getGroups(coa) for _, dcsGroup in pairs(dcsGroups) do table.insert(allDcsGroups, dcsGroup) end end return allDcsGroups end function veaf.getStaticsOfCoalition(coa) local coalitions = { coalition.side.RED, coalition.side.BLUE, coalition.side.NEUTRAL} if coa then coalitions = { coa } end local allDcsStatics = {} for _, coa in pairs(coalitions) do local dcsStatics = coalition.getStaticObjects(coa) for _, dcsStatic in pairs(dcsStatics) do table.insert(allDcsStatics, dcsStatic) end end return allDcsStatics end function veaf.getUnitsOfAllCoalitions(includeStatics) return veaf.getUnitsOfCoalition(includeStatics) end function veaf.getUnitsOfCoalition(includeStatics, coa) local allDcsUnits = {} local allDcsGroups = veaf.getGroupsOfCoalition(coa) for _, group in pairs(allDcsGroups) do for _, unit in pairs(group:getUnits()) do table.insert(allDcsUnits, unit) end end if includeStatics then local allDcsStatics = veaf.getStaticsOfCoalition(coa) for _, staticUnit in pairs(allDcsStatics) do table.insert(allDcsUnits, staticUnit) end end return allDcsUnits end function veaf.getUnitsNamesOfCoalition(includeStatics, coa) local allDcsUnits = {} local allDcsGroups = veaf.getGroupsOfCoalition(coa) for _, group in pairs(allDcsGroups) do for _, unit in pairs(group:getUnits()) do table.insert(allDcsUnits, unit:getName()) end end if includeStatics then local allDcsStatics = veaf.getStaticsOfCoalition(coa) for _, staticUnit in pairs(allDcsStatics) do table.insert(allDcsUnits, StaticObject.getName(staticUnit)) end end return allDcsUnits end function veaf.findUnitsInCircle(center, radius, includeStatics, onlyTheseUnits) veaf.loggers.get(veaf.Id):trace(string.format("findUnitsInCircle(radius=%s)", tostring(radius))) veaf.loggers.get(veaf.Id):trace(string.format("center=%s", veaf.p(center))) if not center then veaf.loggers.get(veaf.Id):error("veaf.findUnitsInCircle: center parameter is nil") return {} end local unitsToCheck = {} if onlyTheseUnits then for k = 1, #onlyTheseUnits do local unit = Unit.getByName(onlyTheseUnits[k]) or StaticObject.getByName(onlyTheseUnits[k]) if unit then unitsToCheck[#unitsToCheck + 1] = unit end end else unitsToCheck = veaf.getUnitsOfAllCoalitions(includeStatics) end local result = {} for _, unit in pairs(unitsToCheck) do local pos = unit:getPosition().p if pos then -- you never know O.o local name = unit:getName() local distanceFromCenter = ((pos.x - center.x)^2 + (pos.z - center.z)^2)^0.5 veaf.loggers.get(veaf.Id):trace(string.format("name=%s; distanceFromCenter=%s", tostring(name), veaf.p(distanceFromCenter))) if distanceFromCenter <= radius then result[name] = unit end end end return result end --- modified version of mist.getGroupRoute that returns raw DCS group data function veaf.getGroupData(groupIdent) -- refactor to search by groupId and allow groupId and groupName as inputs local gpId = groupIdent if mist.DBs.MEgroupsByName[groupIdent] then gpId = mist.DBs.MEgroupsByName[groupIdent].groupId else veaf.loggers.get(veaf.Id):info(groupIdent..' not found in mist.DBs.MEgroupsByName') end for coa_name, coa_data in pairs(env.mission.coalition) do if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do for obj_type_name, obj_type_data in pairs(cntry_data) do if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! for group_num, group_data in pairs(obj_type_data.group) do if group_data and group_data.groupId == gpId then -- this is the group we are looking for return group_data end end end end end end end end end veaf.loggers.get(veaf.Id):info(' no group data found for '..groupIdent) return nil end function veaf.findInTable(data, key) local result = nil if data then result = data[key] end if result then veaf.loggers.get(veaf.Id):trace(".findInTable found ".. key) end return result end function veaf.getTankerData(tankerGroupName) veaf.loggers.get(veaf.Id):trace("getTankerData " .. tankerGroupName) local result = nil local tankerData = veaf.getGroupData(tankerGroupName) if tankerData then result = {} -- find callsign local units = veaf.findInTable(tankerData, "units") if units and units[1] then local callsign = veaf.findInTable(units[1], "callsign") if callsign then local name = veaf.findInTable(callsign, "name") if name then result.tankerCallsign = name end end end -- find frequency local communication = veaf.findInTable(tankerData, "communication") if communication == true then local frequency = veaf.findInTable(tankerData, "frequency") if frequency then result.tankerFrequency = frequency end end local route = veaf.findInTable(tankerData, "route") local points = veaf.findInTable(route, "points") if points then veaf.loggers.get(veaf.Id):trace("found a " .. #points .. "-points route for tanker " .. tankerGroupName) for i, point in pairs(points) do veaf.loggers.get(veaf.Id):trace("found point #" .. i) local task = veaf.findInTable(point, "task") if task then local tasks = task.params.tasks if (tasks) then veaf.loggers.get(veaf.Id):trace("found " .. #tasks .. " tasks") for j, task in pairs(tasks) do veaf.loggers.get(veaf.Id):trace("found task #" .. j) if task.params then veaf.loggers.get(veaf.Id):trace("has .params") if task.params.action then veaf.loggers.get(veaf.Id):trace("has .action") if task.params.action.params then veaf.loggers.get(veaf.Id):trace("has .params") if task.params.action.params.channel then veaf.loggers.get(veaf.Id):trace("has .channel") veaf.loggers.get(veaf.Id):info("Found a TACAN task for tanker " .. tankerGroupName) result.tankerTacanTask = task result.tankerTacanChannel = task.params.action.params.channel result.tankerTacanMode = task.params.action.params.modeChannel break end end end end end end end end end end return result end function veaf.getCarrierATCdata(carrierGroupName, carrierUnitName) veaf.loggers.get(veaf.Id):trace("getCarrierData Group: " .. carrierGroupName .. " Unit: " .. carrierUnitName) local result = nil local carrierData = veaf.getGroupData(carrierGroupName) if carrierData then result = {} -- find carrier unit within group and gather the information local units = veaf.findInTable(carrierData, "units") local carrierUnitId = nil if units then for _,unit in pairs(units) do if unit and unit.name and unit.name == carrierUnitName then --get the unit ID which will be used later when searching for ICLS etc. assigned to the carrier itself and get the tower freq/modulation data carrierUnitId = unit.unitId if carrierUnitId then if unit.frequency then local towerString = string.format("%.2f", unit.frequency / 1000000) local towerMod = "AM" if unit.modulation and unit.modulation == 1 then towerMod = "FM" end result.tower = towerString .. " " .. towerMod .. " (Check Freq. Plan)" end end end end end --if the carrier was found and is identifiable if carrierUnitId then --find programmed tasks for the carrier (ACLS, ICLS, etc.) local tasks = veaf.findInTable(carrierData, "tasks") if tasks then veaf.loggers.get(veaf.Id):trace("found " .. #tasks .. " programmed tasks for carrier " .. carrierUnitName .. " in group " .. carrierGroupName) for i, task in pairs(tasks) do if task then veaf.loggers.get(veaf.Id):trace("found task #" .. i) if task.params then veaf.loggers.get(veaf.Id):trace("has .params") if task.params.action then local action = task.params.action veaf.loggers.get(veaf.Id):trace("has .action") if task.params.action.params then local actionParams = task.params.action.params veaf.loggers.get(veaf.Id):trace("action has .params") if task.params.action.params.unitId and task.params.action.params.unitId == carrierUnitId then veaf.loggers.get(veaf.Id):trace("programmed task is linked to carrier unit") if action.id == "ActivateBeacon" and actionParams.channel then veaf.loggers.get(veaf.Id):info("Found a programmed TACAN task for carrier group " .. carrierGroupName) local channel = actionParams.channel local mode = "X" if actionParams.modeChannel and actionParams.modeChannel == "Y" then --should never happen for carriers mode = "Y" end local callsign = "No Code" if actionParams.callsign then callsign = actionParams.callsign end result.tacan = channel .. mode .. " (" .. callsign .. ")" elseif action.id == "ActivateICLS" and actionParams.channel then veaf.loggers.get(veaf.Id):info("Found a programmed ICLS task for carrier group " .. carrierGroupName) result.icls = actionParams.channel elseif action.id == "ActivateLink4" and actionParams.frequency then veaf.loggers.get(veaf.Id):info("Found a programmed Link4 task for carrier group " .. carrierGroupName) result.link4 = string.format("%.2f".."MHz",actionParams.frequency / 1000000) elseif action.id == "ActivateACLS" then veaf.loggers.get(veaf.Id):info("Found a programmed ACLS task for carrier group " .. carrierGroupName) result.acls = true end end end end end end end end end end return result end function veaf.outTextForUnit(unitName, message, duration, forAllGroup) local unitId = nil local groupId = nil if unitName then local unit = Unit.getByName(unitName) if unit then unitId = unit:getID() local group = unit:getGroup() if group then groupId = group:getID() end end end if unitId and not forAllGroup then trigger.action.outTextForUnit(unitId, message, duration) elseif groupId then trigger.action.outTextForGroup(groupId, message, duration) else trigger.action.outText(message, duration) end end function veaf.outTextForGroup(unitName, message, duration) return veaf.outTextForUnit(unitName, message, duration, true) end --- Weather Report. Report pressure QFE/QNH, temperature, wind at certain location. --- stolen from the weatherReport script and modified to fit our usage function veaf.weatherReport(vec3, alt, withLASTE) -- Get Temperature [K] and Pressure [Pa] at vec3. local T local Pqfe if not alt then alt = veaf.getLandHeight(vec3) + 15 -- get the weather at 15m over the ground, as it's done IRL in general aviation end -- At user specified altitude. T,Pqfe=atmosphere.getTemperatureAndPressure({x=vec3.x, y=alt, z=vec3.z}) veaf.loggers.get(veaf.Id):trace(string.format("T = %.1f, Pqfe = %.2f", T,Pqfe)) -- Get pressure at sea level. local _,Pqnh=atmosphere.getTemperatureAndPressure({x=vec3.x, y=0, z=vec3.z}) veaf.loggers.get(veaf.Id):trace(string.format("Pqnh = %.2f", Pqnh)) -- Convert pressure from Pascal to hecto Pascal. Pqfe=Pqfe/100 Pqnh=Pqnh/100 -- Pressure unit conversion hPa --> mmHg or inHg local _Pqnh=string.format("%.2f mmHg (%.2f inHg)", Pqnh * weathermark.hPa2mmHg, Pqnh * weathermark.hPa2inHg) local _Pqfe=string.format("%.2f mmHg (%.2f inHg)", Pqfe * weathermark.hPa2mmHg, Pqfe * weathermark.hPa2inHg) -- Temperature unit conversion: Kelvin to Celsius or Fahrenheit. T=T-273.15 local _T=string.format('%d°C (%d°F)', T, weathermark._CelsiusToFahrenheit(T)) -- Get wind direction and speed. local Dir,Vel=weathermark._GetWind(vec3, alt) veaf.loggers.get(veaf.Id):trace(string.format("Dir = %.1f, Vel = %.1f", Dir,Vel)) -- Get Beaufort wind scale. local Bn,Bd=weathermark._BeaufortScale(Vel) -- Formatted wind direction. local Ds = string.format('%03d°', Dir) -- Velocity in player units. local Vs=string.format('%.1f m/s (%.1f kn)', Vel, Vel * weathermark.mps2knots) -- Altitude. local _Alt=string.format("%d m (%d ft)", alt, alt * weathermark.meter2feet) local text="" text=text..string.format("Altitude %s ASL\n",_Alt) text=text..string.format("QFE %.2f hPa = %s\n", Pqfe,_Pqfe) text=text..string.format("QNH %.2f hPa = %s\n", Pqnh,_Pqnh) text=text..string.format("Temperature %s\n",_T) if Vel > 0 then text=text..string.format("Wind from %s at %s (%s)", Ds, Vs, Bd) else text=text.."No wind" end local function getLASTEat(vec3, alt) local T,_=atmosphere.getTemperatureAndPressure({x=vec3.x, y=alt, z=vec3.z}) local Dir,Vel=weathermark._GetWind(vec3, alt) local laste = string.format("\nFL%02d W%03d/%02d T%d", alt * weathermark.meter2feet / 1000, Dir, Vel * weathermark.mps2knots, T-273.15) return laste end if withLASTE then text=text.."\n\nLASTE:" text=text..getLASTEat(vec3, math.floor(((alt * weathermark.meter2feet + 2000)/1000)*1000+500)/weathermark.meter2feet) text=text..getLASTEat(vec3, math.floor(((alt * weathermark.meter2feet + 8000)/1000)*1000+500)/weathermark.meter2feet) text=text..getLASTEat(vec3, math.floor(((alt * weathermark.meter2feet + 16000)/1000)*1000+500)/weathermark.meter2feet) --text=text..getLASTEat(vec3, _Alt + 7500) end return text end local function _initializeCountriesAndCoalitions() veaf.countriesByCoalition={} veaf.coalitionByCountry={} veaf.countriesByName={} veaf.countriesNamesById={} local function _sortByImportance(c1,c2) local importantCountries = { ['usa']=true, ['russia']=true} if c1 then return importantCountries[c1:lower()] end return string.lower(c1) < string.lower(c2) end for coalitionName, countries in pairs(mist.DBs.units) do coalitionName = coalitionName:lower() veaf.loggers.get(veaf.Id):trace("coalitionName=%s", veaf.p(coalitionName)) if not veaf.countriesByCoalition[coalitionName] then veaf.countriesByCoalition[coalitionName]={} end veaf.loggers.get(veaf.Id):trace("countries=%s", veaf.p(countries)) for countryName, country in pairs(countries) do veaf.loggers.get(veaf.Id):trace("country=%s", veaf.p(country)) countryName = countryName:lower() table.insert(veaf.countriesByCoalition[coalitionName], countryName) veaf.coalitionByCountry[countryName]=coalitionName:lower() veaf.countriesByName[countryName] = country veaf.countriesNamesById[country.countryId] = countryName end table.sort(veaf.countriesByCoalition[coalitionName], _sortByImportance) end veaf.loggers.get(veaf.Id):trace("veaf.countriesByCoalition=%s", veaf.p(veaf.countriesByCoalition)) veaf.loggers.get(veaf.Id):trace("veaf.coalitionByCountry=%s", veaf.p(veaf.coalitionByCountry)) veaf.loggers.get(veaf.Id):trace("veaf.countriesByName=%s", veaf.p(veaf.countriesByName)) veaf.loggers.get(veaf.Id):trace("veaf.countriesNamesById=%s", veaf.p(veaf.countriesNamesById)) end function veaf.getCountryId(countryName) veaf.loggers.get(veaf.Id):trace("veaf.getCountryId(%s)", veaf.p(countryName)) if not veaf.countriesByName then _initializeCountriesAndCoalitions() end local countryName = string.lower(countryName or "") local country = veaf.countriesByName[countryName] if country then return country.countryId else return 0 end end function veaf.getCountryName(countryId) veaf.loggers.get(veaf.Id):trace("veaf.getCountryName(%s)", veaf.p(countryId)) if not veaf.coalitionByCountry then _initializeCountriesAndCoalitions() end local countryName = veaf.countriesNamesById[countryId] return countryName end function veaf.getCountryForCoalition(coalition) veaf.loggers.get(veaf.Id):trace("veaf.getCountryForCoalition(coalition=%s)", tostring(coalition)) local coalition = coalition if not coalition then coalition = 1 end local coalitionName = nil if type(coalition) == "number" then if coalition == 1 then coalitionName = "red" elseif coalition == 2 then coalitionName = "blue" else coalitionName = "neutral" end else coalitionName = tostring(coalition) end if coalitionName then coalitionName = coalitionName:lower() else return nil end if not veaf.countriesByCoalition then _initializeCountriesAndCoalitions() end return veaf.countriesByCoalition[coalitionName][1] end function veaf.getCoalitionForCountry(countryName, asNumber) veaf.loggers.get(veaf.Id):trace(string.format("veaf.getCoalitionForCountry(countryName=%s, asNumber=%s)", tostring(countryName), tostring(asNumber))) if countryName then countryName = countryName:lower() else return nil end if not veaf.coalitionByCountry then _initializeCountriesAndCoalitions() end local result = veaf.coalitionByCountry[countryName] if asNumber then if result == 'neutral' then result = 0 end if result == 'red' then result = 1 end if result == 'blue' then result = 2 end end return result end function veaf.getAirbaseForCoalition(airbase_name, coa) local airbase = nil veaf.loggers.get(veaf.Id):trace(string.format("veaf.getAirbaseforCoalition(airbase_name = %s, coa = %s)", veaf.p(airbase_name), veaf.p(coa))) if coa and airbase_name then if type(coa) == 'string' then if coa:lower() == "red" then coa = coalition.side.RED elseif coa:lower() == "blue" then coa = coalition.side.BLUE end end veaf.loggers.get(veaf.Id):trace(string.format("final coalition is = %s", veaf.p(coa))) if (coa == coalition.side.RED or coa == coalition.side.BLUE) and type(airbase_name) == 'string' then local temp = Airbase.getByName(airbase_name) veaf.loggers.get(veaf.Id):trace(string.format("Associed Airbase ID : %s", veaf.p(temp))) if temp then veaf.loggers.get(veaf.Id):trace(string.format("Associed Airbase Coalition : %s", veaf.p(temp:getCoalition()))) if temp:getCoalition() == coa then veaf.loggers.get(veaf.Id):trace(string.format("The Airbase was found and is held by the correct coalition")) airbase = temp end end end end return airbase end veaf.AIRBASES_LIFE0 = {} veaf.STANDARD_CARRIER_LIFE0 = 1000 --this fluctuates a lot from ship to ship, took the lowest veaf.STANDARD_AIRBASE_LIFE0 = 3600 veaf.STANDARD_HELIPAD_LIFE0 = 10000000 veaf.STANDARD_BUILDING_LIFE0 = 3600 function veaf.loadAirbasesLife0() local airbases = world.getAirbases() veaf.loggers.get(veaf.Id):trace(string.format("Loading Life0 of airbases...")) for _,airbase in pairs(airbases) do local airbase_name = airbase:getName() veaf.loggers.get(veaf.Id):trace(string.format("Checking airbase named %s", veaf.p(airbase_name))) veaf.AIRBASES_LIFE0[airbase_name] = veaf.getAirbaseLife(airbase_name, false, true) if veaf.AIRBASES_LIFE0[airbase_name] == 0 then veaf.loggers.get(veaf.Id):trace(string.format("Returned Life0 is 0, discarding result")) veaf.AIRBASES_LIFE0[airbase_name] = nil end end end --This method is used to get the life of any airbase/FARP/Carrier/HeloCarrier etc. through it's unit name. You can choose to have the life returned as a percentage (0 to 1) and also to not automatically adjust/store the maximum lifes of the airbases you might check through loading = true (loading mode is used for the function veaf.loadAirbasesLife0()) --Beware that, some airbases do not posses a life or a life0 to calculate a percentage. This method will return -1 if so. function veaf.getAirbaseLife(airbase_name, percentage, loading) veaf.loggers.get(veaf.Id):trace(string.format("veaf.getAirbaseLife(airbase_name = %s, percentage = %s, loading = %s)", veaf.p(airbase_name), veaf.p(percentage), veaf.p(loading))) local airbase_life = -1 local airbase_life0 = -1 if airbase_name and type(airbase_name) == 'string' then local airbase = Airbase.getByName(airbase_name) veaf.loggers.get(veaf.Id):trace(string.format("Airbase ID : %s", veaf.p(airbase))) if airbase then local airbase_desc = airbase:getDesc() veaf.loggers.get(veaf.Id):trace(string.format("Airbase Desc : %s", veaf.p(airbase_desc))) if airbase_desc and airbase_desc.life and airbase_desc.attributes then airbase_life0 = veaf.AIRBASES_LIFE0[airbase_name] airbase_life = airbase_desc.life -- local AirbaseUnit = StaticObject.getByName(airbase_name) -- if AirbaseUnit then -- veaf.loggers.get(veaf.Id):trace(string.format("Got an AirbaseUnit through StaticObject.getByName(), associated life is %s", veaf.p(AirbaseUnit:getLife()))) -- end if airbase_desc.attributes["AircraftCarrier"] or airbase_desc.attributes["Aircraft Carriers"] or airbase_desc.attributes["HelicopterCarrier"] then local AircraftCarrier_unit = Unit.getByName(airbase_name) veaf.loggers.get(veaf.Id):trace(string.format("Airbase is a Carrier Unit ID : %s", veaf.p(AircraftCarrier_unit))) if AircraftCarrier_unit then --airbase_life0 = AircraftCarrier_unit:getLife0() --returns 0, thanks ED, had to load them at mission start to counter this issue if not airbase_life0 then airbase_life0 = veaf.STANDARD_CARRIER_LIFE0 veaf.loggers.get(veaf.Id):trace(string.format("Carrier doesn't have a Life0 stored yet, using default of %s", veaf.p(veaf.STANDARD_CARRIER_LIFE0))) end airbase_life = AircraftCarrier_unit:getLife() veaf.loggers.get(veaf.Id):trace(string.format("Carrier Life : %s", veaf.p(airbase_life))) end elseif airbase_desc.attributes["Helipad"] and not airbase_life0 then airbase_life0 = veaf.STANDARD_HELIPAD_LIFE0 veaf.loggers.get(veaf.Id):trace(string.format("Helipad doesn't have a Life0 stored yet, using default of %s", veaf.p(veaf.STANDARD_HELIPAD_LIFE0))) elseif airbase_desc.attributes["Airfields"] and not airbase_life0 then airbase_life0 = veaf.STANDARD_AIRBASE_LIFE0 veaf.loggers.get(veaf.Id):trace(string.format("Airfield doesn't have a Life0 stored yet, using default of %s", veaf.p(veaf.STANDARD_AIRBASE_LIFE0))) elseif airbase_desc.attributes["Buildings"] then local BuildingUnit = StaticObject.getByName(airbase_name) veaf.loggers.get(veaf.Id):trace(string.format("Airbase is a Building Unit ID : %s", veaf.p(BuildingUnit))) if BuildingUnit then if not airbase_life0 then airbase_life0 = veaf.STANDARD_BUILDING_LIFE0 veaf.loggers.get(veaf.Id):trace(string.format("Building doesn't have a Life0 stored yet, using default of %s", veaf.p(veaf.STANDARD_BUILDING_LIFE0))) end airbase_life = BuildingUnit:getLife() veaf.loggers.get(veaf.Id):trace(string.format("Building Life : %s", veaf.p(airbase_life))) else airbase_life0 = -1 airbase_life = -1 veaf.loggers.get(veaf.Id):trace(string.format("Building that is an airbase doesn't have any life data, discarding")) end elseif not airbase_life0 then if airbase_life > 0 then airbase_life0 = airbase_life veaf.loggers.get(veaf.Id):trace(string.format("Airbase category does not have a default life0 setting, using life instead")) else airbase_life = -1 airbase_life0 = -1 veaf.loggers.get(veaf.Id):trace(string.format("Airbase category does not have a default life0 setting nor does it have a life, discarding")) end end veaf.loggers.get(veaf.Id):trace(string.format("Airbase Life : %s, Airbase Life0 : %s", veaf.p(airbase_life), veaf.p(airbase_life0))) end end end if airbase_life0 and airbase_life0 > 0 and airbase_life and airbase_life > 0 then local airbase_life_percentage = airbase_life/airbase_life0 if not loading then --if the airbase life percentage is superior to 100%, there standard life0 chosen was obviously wrong and needs updating if airbase_life_percentage > 1 then airbase_life_percentage = 1 veaf.AIRBASES_LIFE0[airbase_name] = airbase_life veaf.loggers.get(veaf.Id):trace(string.format("Storing Life0 = Life for airbase...")) elseif not veaf.AIRBASES_LIFE0[airbase_name] then veaf.AIRBASES_LIFE0[airbase_name] = airbase_life0 veaf.loggers.get(veaf.Id):trace(string.format("Storing default Life0 for airbase type...")) end end if percentage then airbase_life = airbase_life_percentage end end veaf.loggers.get(veaf.Id):trace(string.format("Final Airbase (named %s) Life : %s, isPercentage = %s", veaf.p(airbase_name), veaf.p(airbase_life), veaf.p(percentage))) return airbase_life end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- mission restart at a certain hour of the day ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veaf._endMission(delay1, message1, delay2, message2, delay3, message3) veaf.loggers.get(veaf.Id):trace(string.format("veaf._endMission(delay1=%s, message1=%s, delay2=%s, message2=%s, delay3=%s, message3=%s)", veaf.p(delay1), veaf.p(message1), veaf.p(delay2), veaf.p(message2), veaf.p(delay3), veaf.p(message3))) if not delay1 then -- no more delay, let's end this ! trigger.action.outText("Ending mission !",30) veaf.loggers.get(veaf.Id):info("ending mission") trigger.action.setUserFlag("666", 1) else -- show the message trigger.action.outText(message1,30) -- schedule this function after "delay1" seconds veaf.loggers.get(veaf.Id):info(string.format("schedule veaf._endMission after %d seconds", delay1)) mist.scheduleFunction(veaf._endMission, {delay2, message2, delay3, message3}, timer.getTime()+delay1) end end function veaf._checkForEndMission(endTimeInSeconds, checkIntervalInSeconds, checkMessage, delay1, message1, delay2, message2, delay3, message3) veaf.loggers.get(veaf.Id):trace(string.format("veaf._checkForEndMission(endTimeInSeconds=%s, checkIntervalInSeconds=%s, checkMessage=%s, delay1=%s, message1=%s, delay2=%s, message2=%s, delay3=%s, message3=%s)", veaf.p(endTimeInSeconds), veaf.p(checkIntervalInSeconds), veaf.p(checkMessage), veaf.p(delay1), veaf.p(message1), veaf.p(delay2), veaf.p(message2), veaf.p(delay3), veaf.p(message3))) veaf.loggers.get(veaf.Id):trace(string.format("timer.getAbsTime()=%d", timer.getAbsTime())) if timer.getAbsTime() >= endTimeInSeconds then veaf.loggers.get(veaf.Id):trace("calling veaf._endMission") veaf._endMission(delay1, message1, delay2, message2, delay3, message3) else -- output the message if specified if checkMessage then trigger.action.outText(checkMessage,30) end -- schedule this function after a delay veaf.loggers.get(veaf.Id):trace(string.format("schedule veaf._checkForEndMission after %d seconds", checkIntervalInSeconds)) mist.scheduleFunction(veaf._checkForEndMission, {endTimeInSeconds, checkIntervalInSeconds, checkMessage, delay1, message1, delay2, message2, delay3, message3}, timer.getTime()+checkIntervalInSeconds) end end function veaf.endMissionAt(endTimeHour, endTimeMinute, checkIntervalInSeconds, checkMessage, delay1, message1, delay2, message2, delay3, message3) veaf.loggers.get(veaf.Id):trace(string.format("veaf.endMissionAt(endTimeHour=%s, endTimeMinute=%s, checkIntervalInSeconds=%s, checkMessage=%s, delay1=%s, message1=%s, delay2=%s, message2=%s, delay3=%s, message3=%s)", veaf.p(endTimeHour), veaf.p(endTimeMinute), veaf.p(checkIntervalInSeconds), veaf.p(checkMessage), veaf.p(delay1), veaf.p(message1), veaf.p(delay2), veaf.p(message2), veaf.p(delay3), veaf.p(message3))) local endTimeInSeconds = endTimeHour * 3600 + endTimeMinute * 60 veaf.loggers.get(veaf.Id):trace(string.format("endTimeInSeconds=%d", endTimeInSeconds)) veaf._checkForEndMission(endTimeInSeconds, checkIntervalInSeconds, checkMessage, delay1, message1, delay2, message2, delay3, message3) end function veaf.randomlyChooseFrom(aTable, bias) veaf.loggers.get(veaf.Id):trace(string.format("randomlyChooseFrom(%d):%s",bias or 0, veaf.p(aTable))) if aTable == nil or #aTable == 0 then return nil elseif #aTable == 1 then return aTable[1] end local index = math.floor(math.random(1, #aTable)) + (bias or 0) if index < 1 then index = 1 end if index > #aTable then index = #aTable end veaf.loggers.get(veaf.Id):trace(string.format("index = %s", veaf.p(index))) return aTable[index] end function veaf.safeUnpack(package) if type(package) == 'table' then return unpack(package) else return package end end function veaf.getRandomizableNumeric_random(val) veaf.loggers.get(veaf.Id):trace(string.format("getRandomizableNumeric_random(%s)", tostring(val))) local MIN = 0 local MAX = 99 local nVal = tonumber(val) veaf.loggers.get(veaf.Id):trace("nVal=%s", veaf.p(nVal)) if nVal == nil then local dashPos = string.find(val,"%-") veaf.loggers.get(veaf.Id):trace("dashPos=%s", veaf.p(dashPos)) if dashPos then local lower = val:sub(1, dashPos-1) veaf.loggers.get(veaf.Id):trace("lower=%s", veaf.p(lower)) if lower then lower = tonumber(lower) end if lower == nil then lower = MIN end local upper = val:sub(dashPos+1) veaf.loggers.get(veaf.Id):trace("upper=%s", veaf.p(upper)) if upper then upper = tonumber(upper) end if upper == nil then upper = MAX end nVal = math.random(lower, upper) veaf.loggers.get(veaf.Id):trace("nVal=%s", veaf.p(nVal)) end end veaf.loggers.get(veaf.Id):trace(string.format("nVal=%s", tostring(nVal))) return nVal end function veaf.getRandomizableNumeric_norandom(val) veaf.loggers.get(veaf.Id):trace(string.format("getRandomizableNumeric_norandom(%s)", tostring(val))) local nVal = tonumber(val) veaf.loggers.get(veaf.Id):trace(string.format("nVal=%s", tostring(nVal))) if nVal == nil then if val == "1-2" then nVal = 2 end if val == "1-3" then nVal = 3 end if val == "1-4" then nVal = 3 end if val == "1-5" then nVal = 3 end if val == "2-3" then nVal = 2 end if val == "2-4" then nVal = 3 end if val == "2-5" then nVal = 3 end if val == "3-4" then nVal = 3 end if val == "3-5" then nVal = 4 end if val == "4-5" then nVal = 4 end if val == "5-10" then nVal = 7 end if val == "10-15" then nVal = 12 end end veaf.loggers.get(veaf.Id):trace(string.format("nVal=%s", tostring(nVal))) return nVal end function veaf.getRandomizableNumeric(val) veaf.loggers.get(veaf.Id):trace(string.format("getRandomizableNumeric(%s)", tostring(val))) return veaf.getRandomizableNumeric_random(val) end function veaf.writeLineToTextFile(line, filename, filepath) local l_lfs = lfs if not l_lfs and SERVER_CONFIG and SERVER_CONFIG.getModule then l_lfs = SERVER_CONFIG.getModule("lfs") end local l_io = io if not l_io and SERVER_CONFIG and SERVER_CONFIG.getModule then l_io = SERVER_CONFIG.getModule("io") end local l_os = os if not l_os and SERVER_CONFIG and SERVER_CONFIG.getModule then l_os = SERVER_CONFIG.getModule("os") end local l_filepath = filepath if not l_filepath and l_os then l_filepath = l_os.getenv("VEAF_EXPORT_DIR") if l_filepath then l_filepath = l_filepath .. "\\" end veaf.loggers.get(veaf.Id):debug(string.format("filepath=%s", veaf.p(l_filepath))) end if not l_filepath and l_lfs then l_filepath = l_lfs.writedir() veaf.loggers.get(veaf.Id):debug(string.format("filepath=%s", veaf.p(l_filepath))) end if not l_filepath and l_os then l_filepath = l_os.getenv("TEMP") if l_filepath then l_filepath = l_filepath .. "\\" end veaf.loggers.get(veaf.Id):debug(string.format("filepath=%s", veaf.p(l_filepath))) end if l_filepath == "SERVER_SAVEDGAMES_DIR" then l_filepath = l_lfs.writedir() veaf.loggers.get(veaf.Id):debug(string.format("filepath=%s", veaf.p(l_filepath))) end if not l_filepath then return end local l_filename = l_filepath .. (filename or "default.log") local date = "" if l_os then date = tostring(l_os.date('%Y-%m-%d %H:%M:%S.000')) end veaf.loggers.get(veaf.Id):debug(string.format("filename=%s", veaf.p(l_filename))) local file = l_io.open(l_filename, "a") if file then veaf.loggers.get(veaf.Id):trace(string.format("file:write(%s)", veaf.p(line))) file:write(string.format("[%s] %s\r\n", date, line)) file:close() end end function veaf.exportAsJson(data, name, jsonify, filename, export_path) local l_lfs = lfs if not l_lfs and SERVER_CONFIG and SERVER_CONFIG.getModule then l_lfs = SERVER_CONFIG.getModule("lfs") end local l_io = io if not l_io and SERVER_CONFIG and SERVER_CONFIG.getModule then l_io = SERVER_CONFIG.getModule("io") end local l_os = os if not l_os and SERVER_CONFIG and SERVER_CONFIG.getModule then l_os = SERVER_CONFIG.getModule("os") end local l_export_path = export_path if not l_export_path and l_os then l_export_path = l_os.getenv("VEAF_EXPORT_DIR") if l_export_path then l_export_path = l_export_path .. "\\" end veaf.loggers.get(veaf.Id):debug(string.format("filepath=%s", veaf.p(l_export_path))) end if not l_export_path and l_lfs then l_export_path = l_lfs.writedir() veaf.loggers.get(veaf.Id):debug(string.format("filepath=%s", veaf.p(l_export_path))) end if not l_export_path and l_os then l_export_path = l_os.getenv("TEMP") if l_export_path then l_export_path = l_export_path .. "\\" end veaf.loggers.get(veaf.Id):debug(string.format("filepath=%s", veaf.p(l_export_path))) end if l_export_path == "SERVER_SAVEDGAMES_DIR" then l_export_path = l_lfs.writedir() veaf.loggers.get(veaf.Id):debug(string.format("filepath=%s", veaf.p(l_export_path))) end if not l_export_path then return end local function writeln(file, text) file:write(text.."\r\n") end local filename = filename or name .. ".json" veaf.loggers.get(veaf.Id):trace(string.format("filename=%s", veaf.p(filename))) veaf.loggers.get(veaf.Id):info("Dumping ".. name .." as json to "..filename .. " in ".. l_export_path) local header = '{\n' header = header .. ' "' .. name .. '": [\n' local content = {} for key, value in pairs(data) do local line = jsonify(key, value) veaf.loggers.get(veaf.Id):trace("line=%s", veaf.p(line)) table.insert(content, line) end local footer = '\n' footer = footer .. ']\n' footer = footer .. '}\n' local file = l_io.open(l_export_path .. filename, "w") writeln(file, header) writeln(file, table.concat(content, ",\n")) writeln(file, footer) if file then file:close() end end function veaf.isUnitAlive(unit) return unit and unit:isExist() and unit:isActive() end function veaf.getUnitLifeRelative(unit) if unit and veaf.isUnitAlive(unit) then local unitLife=unit:getLife() local unitLife0 = 0 if unit.getLife0 then -- statics have no life0 unitLife0 = unit:getLife0() end if unitLife0 > 0 then return unitLife/unitLife0 else return unitLife end else return 0 end end function veaf.setServerName(value) veaf.config.SERVER_NAME = value end function veaf.setServerBotChannel(value) veaf.config.DCS_SERVER_BOT_CHANNEL = value end function veaf.getPolygonFromUnits(unitNames) veaf.loggers.get(veaf.Id):debug(string.format("veaf.getPolygonFromUnits()")) veaf.loggers.get(veaf.Id):trace(string.format("unitNames = %s", veaf.p(unitNames))) local polygon = {} for _, unitName in pairs(unitNames) do veaf.loggers.get(veaf.Id):trace(string.format("unitName = %s", veaf.p(unitName))) local unit = Unit.getByName(unitName) if not unit then local group = Group.getByName(unitName) if group then unit = group:getUnit(1) end end if unit then -- get position, place tracing marker and remove the unit local position = unit:getPosition().p unit:destroy() veaf.loggers.get(veaf.Id):trace(string.format("position = %s", veaf.p(position))) table.insert(polygon, mist.utils.deepCopy(position)) end end veaf.loggers.get(veaf.Id):trace(string.format("polygon = %s", veaf.p(polygon))) return polygon end function veaf.laserCodeToDigit(code) local codeDigit = {} codeDigit.units=code%10 codeDigit.tens=(code%100-codeDigit.units)/10 codeDigit.hundreds=(code%1000-codeDigit.tens*10-codeDigit.units)/100 codeDigit.thousands=(code-codeDigit.hundreds*100-codeDigit.tens*10-codeDigit.units)/1000 veaf.loggers.get(veaf.Id):debug(string.format("laser code : %s", veaf.p(code))) veaf.loggers.get(veaf.Id):debug(string.format("laser code digits : %s", veaf.p(codeDigit))) return codeDigit end --computes the heading between two points in radians function veaf.headingBetweenPoints(point1, point2) local hdg if point1 and point2 and point1.x and point1.y and point2.x and point2.y then -- if hdg is not set, compute heading between point2 and point3 hdg = math.floor(math.deg(math.atan2(point2.y - point1.y, point2.x - point1.x))) if hdg < 0 then hdg = hdg + 360 end end -- convert heading to radians hdg = hdg * math.pi / 180 return hdg end ---checks if a string starts with a prefix ---@param aString any ---@param aPrefix any ---@param caseSensitive? boolean ; if true, case sensitive search ---@return boolean function veaf.startsWith(aString, aPrefix, caseSensitive) local aString = aString if not aString then veaf.loggers.get(veaf.Id):error("veaf.startsWith: parameter aString is mandatory") return false elseif not caseSensitive then aString = aString:upper() end local aPrefix = aPrefix if not aPrefix then veaf.loggers.get(veaf.Id):error("veaf.startsWith: parameter aPrefix is mandatory") return false elseif not caseSensitive then aPrefix = aPrefix:upper() end return string.sub(aString,1,string.len(aPrefix))==aPrefix end function veaf.getDcsTypeName(dcsElementName) veaf.loggers.get(veaf.Id):debug("veaf.getDcsTypeName(dcsElementName=%s", veaf.p(dcsElementName)) local result = "unknown" if (not veaf.isNullOrEmpty(dcsElementName)) then -- first check for a unit named like this, because the group and its units may have the same name local dcsUnit = Unit.getByName(dcsElementName) veaf.loggers.get(veaf.Id):trace("Unit.getByName(dcsElementName)=%s", veaf.p(dcsUnit)) if not dcsUnit then -- then check for a group named like that local dcsGroup = Group.getByName(dcsElementName) veaf.loggers.get(veaf.Id):trace("Group.getByName(dcsElementName)=%s", veaf.p(dcsGroup)) dcsUnit = dcsGroup and dcsGroup:getUnit(1) end if (dcsUnit) then veaf.loggers.get(veaf.Id):trace("dcsUnit=%s", veaf.p(dcsUnit, nil, nil, true, false)) result = dcsUnit:getTypeName() end end veaf.loggers.get(veaf.Id):trace("result=%s", veaf.p(result)) return result end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Logging ------------------------------------------------------------------------------------------------------------------------------------------------------------- veaf.loggers = {} veaf.loggers.dict = {} veaf.Logger = { -- technical name name = nil, -- logging level level = nil, } veaf.Logger.__index = veaf.Logger veaf.Logger.LEVEL = { ["error"]=1, ["warning"]=2, ["info"]=3, ["debug"]=4, ["trace"]=5, } function veaf.Logger:new(name, level) local self = setmetatable({}, veaf.Logger) self:setName(name) self:setLevel(level) return self end function veaf.Logger:setName(value) self.name = value return self end function veaf.Logger:getName() return self.name end function veaf.Logger:setLevel(value, force) if veaf.ForcedLogLevel then value = veaf.ForcedLogLevel end local level = value if type(level) == "string" then level = veaf.Logger.LEVEL[level:lower()] end if not level then level = veaf.Logger.LEVEL["info"] end if veaf.BaseLogLevel < level and not force then level = veaf.BaseLogLevel end self.level = level return self end function veaf.Logger:getLevel() return self.level end function veaf.Logger.splitText(text) local tbl = {} while text:len() > 4000 do local sub = text:sub(1, 4000) text = text:sub(4001) table.insert(tbl, sub) end table.insert(tbl, text) return tbl end function veaf.Logger.formatText(text, ...) if not text then return "" end if type(text) ~= 'string' then text = veaf.p(text) else local args = ... if args and args.n and args.n > 0 then local pArgs = {} for i=1,args.n do pArgs[i] = veaf.p(args[i]) end -- add a few empty strings for safety for i = 1, 20 do table.insert(pArgs, "[nil]") end text = text:format(unpack(pArgs)) end end local fName = nil local cLine = nil if debug and debug.getinfo then local dInfo = debug.getinfo(3) fName = dInfo.name cLine = dInfo.currentline -- local fsrc = dinfo.short_src --local fLine = dInfo.linedefined end if fName and cLine then return fName .. '|' .. cLine .. ': ' .. text elseif cLine then return cLine .. ': ' .. text else return ' ' .. text end end function veaf.Logger:print(level, text, logWithDcsServerBot) local texts = veaf.Logger.splitText(text) local levelChar = 'E' local logFunction = env.error if level == veaf.Logger.LEVEL["warning"] then levelChar = 'W' logFunction = env.warning elseif level == veaf.Logger.LEVEL["info"] then levelChar = 'I' logFunction = env.info elseif level == veaf.Logger.LEVEL["debug"] then levelChar = 'D' logFunction = env.info elseif level == veaf.Logger.LEVEL["trace"] then levelChar = 'T' logFunction = env.info end for i = 1, #texts do if i == 1 then local theText = self.name .. '|' .. levelChar .. '|' .. texts[i] logFunction(theText) if logWithDcsServerBot and dcsbot and veaf.config.DCS_SERVER_BOT_CHANNEL then local current_mission = Sim.getMissionName() dcsbot.sendBotMessage(veaf.config.SERVER_NAME .. ' | ' .. current_mission .. ' | ' .. theText, veaf.config.DCS_SERVER_BOT_CHANNEL) end else local theText = texts[i] logFunction(theText) if logWithDcsServerBot and dcsbot and veaf.config.DCS_SERVER_BOT_CHANNEL then dcsbot.sendBotMessage(theText, veaf.config.DCS_SERVER_BOT_CHANNEL) end end end end function veaf.Logger:error(text, ...) if self.level >= 1 then text = veaf.Logger.formatText(text, arg) local mText = text if debug and debug.traceback then mText = mText .. "\n" .. debug.traceback() end self:print(1, mText, true) end end function veaf.Logger:warn(text, ...) if self.level >= 2 then text = veaf.Logger.formatText(text, arg) self:print(2, text) end end function veaf.Logger:info(text, ...) if self.level >= 3 then text = veaf.Logger.formatText(text, arg) self:print(3, text) end end function veaf.Logger:debug(text, ...) if self.level >= 4 then text = veaf.Logger.formatText(text, arg) self:print(4, text) end end function veaf.Logger:trace(text, ...) if self.level >= 5 then text = veaf.Logger.formatText(text, arg) self:print(5, text) end end function veaf.Logger:wouldLogWarn() return self.level >= 2 end function veaf.Logger:wouldLogInfo() return self.level >= 3 end function veaf.Logger:wouldLogDebug() return self.level >= 4 end function veaf.Logger:wouldLogTrace() return self.level >= 5 end function veaf.Logger:marker(id, header, message, position, markersTable, radius, fillColor) if not id then id = 99999 end if self.level >= 5 then local correctedPos = {} correctedPos.x = position.x if not(position.z) then correctedPos.z = position.y correctedPos.y = position.alt else correctedPos.z = position.z correctedPos.y = position.y end if not (correctedPos.y) then correctedPos.y = 0 end local message = message if header and id then message = header..id.." "..message end self:trace("creating trace marker #%s at point %s", id, veaf.vecToString(correctedPos)) if radius then trigger.action.circleToAll(-1, id, correctedPos, radius, fillColor, fillColor, 3, false) else trigger.action.markToAll(id, message, correctedPos, false) end if markersTable then table.insert(markersTable, id) --self:trace("markersTable=%s", veaf.p(markersTable)) end end return id + 1 end function veaf.Logger:markerArrow(id, header, message, positionStart, positionEnd, markersTable, lineType, fillColor) if not id then id = 99999 end if self.level >= 5 then local points = { positionStart, positionEnd } for _, point in ipairs(points) do local correctedPos = {} correctedPos.x = point.x if not(point.z) then correctedPos.z = point.y correctedPos.y = point.alt else correctedPos.z = point.z correctedPos.y = point.y end if not (correctedPos.y) then correctedPos.y = 0 end point.x = correctedPos.x point.y = correctedPos.y point.z = correctedPos.z end local positionStart = points[1] local positionEnd = points[2] local message = message if header and id then message = header..id.." "..message end self:trace("creating trace arrow #%s from point %s to point %s", id, veaf.vecToString(positionStart), veaf.vecToString(positionEnd)) trigger.action.arrowToAll(-1, id, positionEnd, positionStart, fillColor, fillColor, lineType, false, message) if markersTable then table.insert(markersTable, id) --self:trace("markersTable=%s", veaf.p(markersTable)) end end return id + 1 end function veaf.Logger:markerQuad(id, header, message, points, markersTable, lineType, fillColor) if not id then id = 99999 end if self.level >= 5 then local points = points for _, point in ipairs(points) do local correctedPos = {} correctedPos.x = point.x if not(point.z) then correctedPos.z = point.y correctedPos.y = point.alt else correctedPos.z = point.z correctedPos.y = point.y end if not (correctedPos.y) then correctedPos.y = 0 end point.x = correctedPos.x point.y = correctedPos.y point.z = correctedPos.z end local message = message if header and id then message = header..id.." "..message end self:trace("creating trace quad #%s", id) trigger.action.quadToAll(-1, id, points[1], points[2], points[3], points[4], fillColor, fillColor, lineType, false, message) if markersTable then table.insert(markersTable, id) --self:trace("markersTable=%s", veaf.p(markersTable)) end end return id + 1 end function veaf.Logger:cleanupMarkers(markersTable) local n=#markersTable for i=1,n do local markerId = markersTable[i] markersTable[i] = nil self:trace("deleting trace marker #%s at pos", markerId, i) trigger.action.removeMark(markerId) end end function veaf.loggers.setBaseLevel(level) veaf.BaseLogLevel = level -- reset all loggers level if lower than the base level for name, logger in pairs(veaf.loggers.dict) do logger:setLevel(logger:getLevel()) end end function veaf.loggers.new(loggerId, level) if not loggerId or #loggerId == 0 then return nil end local result = veaf.Logger:new(loggerId:upper(), level) veaf.loggers.dict[loggerId:lower()] = result return result end function veaf.loggers.get(loggerId) local result = nil if loggerId and #loggerId > 0 then result = veaf.loggers.dict[loggerId:lower()] end if not result then result = veaf.loggers.get("veaf") end return result end if veaf.Development then veaf.loggers.setBaseLevel(veaf.Logger.LEVEL["trace"]) end veaf.loggers.new(veaf.Id, veaf.LogLevel) ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- unique identifers ------------------------------------------------------------------------------------------------------------------------------------------------------------- veaf.UNIQUE_ID = 10000 + math.random(50,500) function veaf.getUniqueIdentifier() veaf.UNIQUE_ID = veaf.UNIQUE_ID + 1 return veaf.UNIQUE_ID end function veaf.generateMilitaryGroupName() -- Different naming patterns local patterns = { "adjective_animal", "adjective_weapon", "number_adjective_noun", "callsign_squad", "geographic_unit", "mythological", "tactical_designation" } -- Word lists local adjectives = { "Iron", "Steel", "Thunder", "Lightning", "Storm", "Fire", "Ice", "Shadow", "Ghost", "Viper", "Wolf", "Eagle", "Hawk", "Razor", "Crimson", "Silver", "Golden", "Black", "Red", "Blue", "Elite", "Special", "Heavy", "Swift", "Silent", "Deadly", "Fierce", "Savage", "Wild", "Noble", "Brave", "Bold" } local animals = { "Wolf", "Eagle", "Hawk", "Lion", "Tiger", "Bear", "Viper", "Cobra", "Falcon", "Raven", "Panther", "Jaguar", "Shark", "Scorpion", "Spider", "Rhino", "Buffalo", "Stallion", "Hound", "Fox", "Lynx", "Wolverine" } local weapons = { "Sword", "Blade", "Lance", "Spear", "Arrow", "Bolt", "Hammer", "Axe", "Dagger", "Rifle", "Cannon", "Missile", "Torpedo", "Sabre", "Javelin" } local nouns = { "Battalion", "Regiment", "Division", "Brigade", "Company", "Platoon", "Squad", "Unit", "Force", "Guard", "Rangers", "Commandos", "Marines", "Troopers", "Warriors", "Fighters", "Soldiers", "Knights", "Legion" } local callsigns = { "Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Tango", "Uniform", "Victor", "Whiskey", "X-ray" } local geographic = { "Mountain", "Desert", "Forest", "Arctic", "Coastal", "Highland", "Valley", "Ridge", "Peak", "Storm", "Frost", "Dune", "Mesa", "Canyon", "River" } local mythological = { "Phoenix", "Dragon", "Griffin", "Hydra", "Kraken", "Valkyrie", "Titan", "Cerberus", "Pegasus", "Chimera", "Minotaur", "Cyclops", "Banshee" } local tactical = { "Recon", "Assault", "Strike", "Support", "Heavy", "Light", "Stealth", "Rapid", "Mobile", "Shock", "Elite", "Special", "Advanced", "Combat" } -- Helper function to get random element from table local function getRandomElement(tbl) return tbl[math.random(#tbl)] end -- Select random pattern local pattern = getRandomElement(patterns) local name = "" if pattern == "adjective_animal" then name = getRandomElement(adjectives) .. " " .. getRandomElement(animals) elseif pattern == "adjective_weapon" then name = getRandomElement(adjectives) .. " " .. getRandomElement(weapons) elseif pattern == "number_adjective_noun" then local number = math.random(1, 99) local suffix = "th" if number % 10 == 1 and number ~= 11 then suffix = "st" elseif number % 10 == 2 and number ~= 12 then suffix = "nd" elseif number % 10 == 3 and number ~= 13 then suffix = "rd" end name = number .. suffix .. " " .. getRandomElement(adjectives) .. " " .. getRandomElement(nouns) elseif pattern == "callsign_squad" then name = getRandomElement(callsigns) .. " " .. getRandomElement(nouns) elseif pattern == "geographic_unit" then name = getRandomElement(geographic) .. " " .. getRandomElement(nouns) elseif pattern == "mythological" then name = getRandomElement(mythological) .. " " .. getRandomElement(nouns) elseif pattern == "tactical_designation" then name = getRandomElement(tactical) .. " " .. getRandomElement(adjectives) .. " " .. getRandomElement(nouns) end return name end function veaf.getNameForSpawnedGroup(pCoalition, pBaseName, pCombatZoneName) local groupNameTemplate = "%s%s-%s#%s" local coaStr = "[n]" if pCoalition == coalition.side.RED then coaStr = "[r]" elseif pCoalition == coalition.side.BLUE then coaStr = "[b]" end local baseName = pBaseName local combatZoneName = pCombatZoneName if veaf.HideNamesFromSpawnedGroups or not baseName or baseName == "" then baseName = veaf.generateMilitaryGroupName() end if veaf.HideNamesFromSpawnedGroups or not combatZoneName then combatZoneName = nil end if combatZoneName then return string.format("%s %s %s#%s", combatZoneName, coaStr, baseName, veaf.getUniqueIdentifier()) else return string.format("%s-%s#%s", coaStr, baseName, veaf.getUniqueIdentifier()) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- lines and figures on the map ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafDrawingOnMap = {} VeafDrawingOnMap.DEFAULT_COLOR = {170/255, 10/255, 0/255, 220/255} VeafDrawingOnMap.DEFAULT_FILLCOLOR = {170/255, 10/255, 0/255, 170/255} function VeafDrawingOnMap.init(object) -- technical name (identifier) object.name = nil -- coalition object.coalition = coalition.side.BLUE -- points forming the drawing object.points = {} -- color ({r, g, b, a}) object.color = VeafDrawingOnMap.DEFAULT_COLOR -- fill color ({r, g, b, a}) object.fillColor = VeafDrawingOnMap.DEFAULT_FILLCOLOR -- type of line (member of VeafDrawingOnMap.LINE_TYPE) object.lineType = VeafDrawingOnMap.LINE_TYPE["solid"] -- if true, the line is an arrow object.isArrow = false -- marker ids object.dcsMarkerIds = {} end -- Type of line marking the zone -- 0 No Line -- 1 Solid -- 2 Dashed -- 3 Dotted -- 4 Dot Dash -- 5 Long Dash -- 6 Two Dash VeafDrawingOnMap.LINE_TYPE = { ["none"] = 0, ["solid"] = 1, ["dashed"] = 2, ["dotted"] = 3, ["dotdash"] = 4, ["longdash"] = 5, ["twodashes"] = 6 } VeafDrawingOnMap.COLORS = { ["transparent"] = {0, 0, 0, 0}, ["black"] = {0, 0, 0, 1}, ["white"] = {1, 1, 1, 1}, ["red"] = {1, 0, 0, 1}, ["pink"] = {1, 0, 0, 0.3}, ["green"] = {0, 1, 0, 1}, ["blue"] = {0, 0, 1, 1} } function VeafDrawingOnMap:new(objectToCopy) veaf.loggers.get(veaf.Id):debug("VeafDrawingOnMap:new()") local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object VeafDrawingOnMap.init(objectToCreate) return objectToCreate end function VeafDrawingOnMap:setName(value) veaf.loggers.get(veaf.Id):trace("VeafDrawingOnMap[]:setName(%s)", veaf.p(value)) self.name = value return self end function VeafDrawingOnMap:getName() return self.name end function VeafDrawingOnMap:setCoalition(value) veaf.loggers.get(veaf.Id):trace("VeafDrawingOnMap[%s]:setCoalition(%s)", veaf.p(self:getName()), veaf.p(value)) self.coalition = value return self end function VeafDrawingOnMap:getCoalition() return self.coalition end function VeafDrawingOnMap:addPoint(value) veaf.loggers.get(veaf.Id):trace("VeafDrawingOnMap[%s]:addPoint(%s)", veaf.p(self.name), veaf.p(value)) table.insert(self.points, 1, mist.utils.deepCopy(value)) return self end function VeafDrawingOnMap:addPoints(value) veaf.loggers.get(veaf.Id):trace("VeafDrawingOnMap[%s]:addPoints(%s)", veaf.p(self.name), veaf.p(value)) if value and #value > 0 then for _, item in pairs(value) do self:addPoint(item) end end return self end function VeafDrawingOnMap:setPointsFromUnits(unitNames) veaf.loggers.get(veaf.Id):debug("VeafDrawingOnMap[%s]:setPointsFromUnits()", veaf.p(self.name)) local polygon = veaf.getPolygonFromUnits(unitNames) self:addPoints(polygon) return self end function VeafDrawingOnMap:setColor(value) veaf.loggers.get(veaf.Id):trace("VeafDrawingOnMap[%s]:setColor(%s)", veaf.p(self:getName()), veaf.p(value)) if value and type(value) == "string" then value = VeafDrawingOnMap.COLORS[value:lower()] end if value then self.color = mist.utils.deepCopy(value) end return self end function VeafDrawingOnMap:setFillColor(value) veaf.loggers.get(veaf.Id):trace("VeafDrawingOnMap[%s]:setFillColor(%s)", veaf.p(self:getName()), veaf.p(value)) if value and type(value) == "string" then value = VeafDrawingOnMap.COLORS[value:lower()] end if value then self.fillColor = mist.utils.deepCopy(value) end return self end function VeafDrawingOnMap:setLineType(value) veaf.loggers.get(veaf.Id):trace("VeafDrawingOnMap[%s]:setLineType(%s)", veaf.p(self:getName()), veaf.p(value)) if value and type(value) == "string" then value = VeafDrawingOnMap.LINE_TYPE[value:lower()] end if value then self.lineType = value end return self end function VeafDrawingOnMap:setArrow() veaf.loggers.get(veaf.Id):trace("VeafDrawingOnMap[%s]:setArrow()", veaf.p(self:getName())) self.isArrow = true return self end function VeafDrawingOnMap:draw() veaf.loggers.get(veaf.Id):trace("VeafDrawingOnMap[%s]:draw()", veaf.p(self:getName())) -- start by erasing the drawing if it already is drawn self:erase() -- then draw it local lastPoint = nil local firstPoint = nil for _, point in pairs(self.points) do veaf.loggers.get(veaf.Id):trace("drawing line [%s] - [%s]", veaf.p(lastPoint), veaf.p(point)) local id = veaf.getUniqueIdentifier() if lastPoint then veaf.loggers.get(veaf.Id):trace("id=[%s]", veaf.p(id)) if self.isArrow then trigger.action.arrowToAll(self:getCoalition(), id, lastPoint, point, self.color, self.fillColor, self.lineType, true) else trigger.action.lineToAll(self:getCoalition(), id, lastPoint, point, self.color, self.lineType, true) end else veaf.loggers.get(veaf.Id):trace("setting firstPoint to [%s]", veaf.p(point)) trigger.action.markToCoalition(id, self.name, point, self.coalition, true, nil) firstPoint = point end table.insert(self.dcsMarkerIds, id) lastPoint = point end -- finish the polygon if firstPoint and lastPoint and #self.points > 2 and not self.isArrow then veaf.loggers.get(veaf.Id):trace("finishing the polygon") local id = veaf.getUniqueIdentifier() veaf.loggers.get(veaf.Id):trace("id=[%s]", veaf.p(id)) if self.isArrow then trigger.action.arrowToAll(self:getCoalition(), id, lastPoint, firstPoint, self.color, self.fillColor, self.lineType, true) else trigger.action.lineToAll(self:getCoalition(), id, lastPoint, firstPoint, self.color, self.lineType, true) end table.insert(self.dcsMarkerIds, id) end return self end function VeafDrawingOnMap:erase() veaf.loggers.get(veaf.Id):trace("VeafDrawingOnMap[%s]:erase()", veaf.p(self:getName())) if self.dcsMarkerIds then for _, id in pairs(self.dcsMarkerIds) do veaf.loggers.get(veaf.Id):trace("removing mark id=[%s]", veaf.p(id)) trigger.action.removeMark(id) end end return self end VeafCircleOnMap = VeafDrawingOnMap:new() function VeafCircleOnMap.init(object) -- inheritance VeafDrawingOnMap.init(object) -- radius in meters object.radius = nil end function VeafCircleOnMap:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object VeafCircleOnMap.init(objectToCreate) return objectToCreate end function VeafCircleOnMap:setCenter(value) veaf.loggers.get(veaf.Id):trace("VeafCircleOnMap[%s]:setCenter(%s)", veaf.p(self.name), veaf.p(value)) self.points = { mist.utils.deepCopy(value) } return self end function VeafCircleOnMap:setRadius(value) veaf.loggers.get(veaf.Id):trace("VeafCircleOnMap[%s]:setRadius(%s)", veaf.p(self.name), veaf.p(value)) self.radius = value return self end function VeafCircleOnMap:draw() veaf.loggers.get(veaf.Id):trace("VeafCircleOnMap[%s]:draw()", veaf.p(self:getName())) -- start by erasing the drawing if it already is drawn self:erase() -- then draw it local id = veaf.getUniqueIdentifier() veaf.loggers.get(veaf.Id):trace("id=[%s]", veaf.p(id)) trigger.action.circleToAll(self:getCoalition(), id , self.points[1], self.radius , self.color, self.fillColor, self.lineType, true) table.insert(self.dcsMarkerIds, id) return self end VeafSquareOnMap = VeafDrawingOnMap:new() function VeafSquareOnMap.init(object) -- inheritance VeafDrawingOnMap.init(object) -- side length in meters object.side = nil -- center of the square object.center = nil end function VeafSquareOnMap:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object VeafSquareOnMap.init(objectToCreate) return objectToCreate end function VeafSquareOnMap:setCenter(value) veaf.loggers.get(veaf.Id):trace("VeafSquareOnMap[%s]:setCenter(%s)", veaf.p(self.name), veaf.p(value)) self.center = mist.utils.deepCopy(value) self:compute() return self end function VeafSquareOnMap:setSide(value) veaf.loggers.get(veaf.Id):trace("VeafSquareOnMap[%s]:setSide(%s)", veaf.p(self.name), veaf.p(value)) self.side = value self:compute() return self end function VeafSquareOnMap:compute() veaf.loggers.get(veaf.Id):trace("VeafSquareOnMap[%s]:compute()", veaf.p(self.name)) if self.side and self.center then veaf.loggers.get(veaf.Id):trace("self.center=%s", veaf.p(self.center)) veaf.loggers.get(veaf.Id):trace("self.side=%s", veaf.p(self.side)) local leftDownPoint = { x = self.center.x - self.side / 2, y = self.center.y, z = self.center.z - self.side / 2 } veaf.loggers.get(veaf.Id):trace("leftDownPoint=%s", veaf.p(leftDownPoint)) local rightUpPoint = { x = self.center.x + self.side / 2, y = self.center.y,z = self.center.z + self.side / 2 } veaf.loggers.get(veaf.Id):trace("rightUpPoint=%s", veaf.p(rightUpPoint)) self.points = { leftDownPoint, rightUpPoint } end return self end function VeafSquareOnMap:draw() veaf.loggers.get(veaf.Id):trace("VeafSquareOnMap[%s]:draw()", veaf.p(self:getName())) -- start by erasing the drawing if it already is drawn self:erase() -- then draw it local id = veaf.getUniqueIdentifier() veaf.loggers.get(veaf.Id):trace("id=[%s]", veaf.p(id)) trigger.action.rectToAll(self:getCoalition(), id , self.points[1], self.points[2], self.color, self.fillColor, self.lineType, true) table.insert(self.dcsMarkerIds, id) return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- trigger zones management ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veaf._discoverTriggerZones() for _, zones in pairs(env.mission.triggers) do for _, zoneData in pairs(zones) do veaf.triggerZones[zoneData.name] = { ["radius"] = zoneData.radius, ["zoneId"] = zoneData.zoneId, ["color"] = { [1] = zoneData.color[1], [2] = zoneData.color[2], [3] = zoneData.color[3], [4] = zoneData.color[4], }, ["properties"] = zoneData.properties, ["hidden"] = zoneData.hidden, ["y"] = zoneData.y, ["x"] = zoneData.x, ["name"] = zoneData.name, ["type"] = zoneData.type, } if zoneData.type == 2 then veaf.triggerZones[zoneData.name].verticies = zoneData.verticies end end end end function veaf.getTriggerZone(zoneName) return veaf.triggerZones[zoneName] end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialize the random number generator to make it almost random math.random(); math.random(); math.random() local l_os = os if not l_os and SERVER_CONFIG and SERVER_CONFIG.getModule then l_os = SERVER_CONFIG.getModule("os") end if l_os and l_os.time and math.randomseed then math.randomseed(l_os.time()) end --- Enable/Disable error boxes displayed on screen. env.setErrorMessageBoxEnabled(false) veaf.loggers.get(veaf.Id):info("Loading version %s", veaf.Version) veaf.loggers.get(veaf.Id):info("veaf.Development=%s", veaf.Development) veaf.loggers.get(veaf.Id):info("veaf.SecurityDisabled=%s", veaf.SecurityDisabled) veaf.loggers.get(veaf.Id):info("veaf.LogLevel=%s", veaf.LogLevel) veaf.loggers.get(veaf.Id):info("veaf.ForcedLogLevel=%s", veaf.ForcedLogLevel) -- discover trigger zones veaf._discoverTriggerZones() --store maximum airbase lifes veaf.loadAirbasesLife0() ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- changes to AIEN ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Our AIEN_xcl_tag (VEAF version) does not autoinitialize. It's also set to log messages using the VEAF logging functions -- Instead, we count on the mission makers to call AIEN.performPhaseCycle() from the missionConfig.lua file (since v5.0) -- Here, we're upgrading the vanilla AIEN configuration to adapt it to our preferred defaults if AIEN then AIEN.Id = "AIEN" --AIEN.LogLevel = "info" AIEN.LogLevel = "debug" --AIEN.LogLevel = "trace" AIEN.logger = veaf.loggers.new(AIEN.Id, AIEN.LogLevel) AIEN.loggers = veaf.loggers -- replace AIEN loggers with ours -- coalition affected by the script AIEN.config.blueAI = true -- true/false. If true, the AI enhancement will be applied to the blue coalition ground groups, else, no script effect will take place AIEN.config.redAI = true -- true/false. If true, the AI enhancement will be applied to the red coalition ground groups, else, no script effect will take place -- Action sets allowed. AIEN.config.suppression = true -- true/false. If true, once a group take fire from arty or air and it's not armoured, it will be suppressed for 15-45 seconds and won't return fire. Require reactions to be set as 'true' AIEN.config.firemissions = true -- true/false. If true, each artillery in the coalition will fire automatically at available targets provided by other ground units and drones AIEN.config.reactions = true -- true/false. If true, when a mover group gets an hit, it will react accordingly to its skills and to its situational awareness, not staying there taking hits without doing nothing AIEN.config.dismount = true -- true/false. //BEWARE: CAN AFFECT PERFORMANCES ON LOW END SYSTEMS // Thanks to MBot's original script, if true AI ground units with infantry transport capabilities (mainly APC/IFV/Trucks) will dismount soldiers with rifle, rpg and sometimes mandpads when appropriate -- User advanced customization AIEN.config.AIEN_xcl_tag = "XCL" -- string, global, case sensitive. Can be dynamically changed by other script or triggers, since it's a global variable. used as a text format without spaces or special characters. only letters and numbers allowed. Any ground group with this 'tag' in its group name won't get AI enhancement behaviour, regardless of its coalition AIEN.config.AIEN_zoneFilter = "" -- string, global, case sensitive. Can be dynamically changed by other script or triggers, since it's a global variable. used as a text format without spaces or special characters. only letters and numbers allowed, i.e. "AIEN" will fit. If left nil, or void string like "", won't be used. Only groups inside the named trigger zone will be affected by AIEN script behaviors of reaction, dismount and suppression, and vice versa. If no trigger zone with the specific name is in the mission, then all the groups will use AIEN features. AIEN.config.message_feed = true -- true/false. If true, each relevant AI action starting will also create a trigger message feedback for its coalition AIEN.config.mark_on_f10_map = true -- true/false. If true, when an artillery fire mission is ongoing, a markpoint will appear on the map of the allied coalition to show the expected impact point AIEN.config.skill_action_const = false -- true/false. If true, AI available reactions types will be limited by the group average skill. If not, almost 2/3 of all available actions will be always be available regardless of the group skills -- User bug report: prior to report a bug, please try reproducing it with this variable set to "true" AIEN.config.AIEN_debugProcessDetail = true -- movement variables AIEN.config.outRoadSpeed = 8 -- do *3.6 for km/h, cause DCS thinks in m/s AIEN.config.inRoadSpeed = 15 -- do *3.6 for km/h, cause DCS thinks in m/s AIEN.config.infantrySpeed = 2 -- do *3.6 for km/h, cause DCS thinks in m/s AIEN.config.repositionDistance = 500 -- meters, radius to a specific destination point that will be randomized between 90% and 110% of this value. Used when a group is moved upon another group position: the other group position will be the destination. AIEN.config.rndFleeDistance = 2000 -- meters, reposition distance given to a group when a destination is not defined. The direction also will be totally random. Used, i.e., for "panic" reaction -- dismounted troops variables AIEN.config.droppedReposition = 80 -- if no enemy is identified, this is the distance where dismount group will reposition themselves AIEN.config.remountTime = 600 -- time after which dismounted troops will try to go back to their original vehicle for remount, if commanded AIEN.config.infantryExtractDist = 200 -- max distance from vehicle to troops to allow a group extraction AIEN.config.infantrySearchDist = 2000 -- max distance from vehicle to troops to allow a dismount group to run toward the enemies -- informative calls variables AIEN.config.outAmmoLowLevel = 0.5 -- factor on total amount -- reactions and tasking variables AIEN.config.intelDbTimeout = 1200 -- seconds. Used to cancel intelDb entries for units (not static!), when the time of the contact gathering is more than this value AIEN.config.artyFireLastContactThereshold = 300 -- seconds, max amount of time since last contact to consider an arty target ok AIEN.config.taskTimeout = 480 -- seconds after which a tasked group is removed from the database AIEN.config.targetedTimeout = 240 -- seconds after which a targeted variable in inteldb is removed from database AIEN.config.disperseActionTime = 120 -- seconds AIEN.config.counterBatteryRadarRange = 50000 -- m, capable distance for a radar to perform counter battery calculations AIEN.config.counterBatteryPlanDelay = 240 -- s, will be also randomized on +-35%. Used to define the delay of the planned counter battery fire if available AIEN.config.smoke_source_num = 5 -- number, between 4 and 9. Generated smokes for each unit when smoke reaction is called in. Any number below 4 or above 9 will be converted in the nearest threshold -- SA evaluation variables AIEN.config.proxyBuildingDistance = 4000 -- m, if buildings are within this distance value, they are considered "close" AIEN.config.proxyUnitsDistance = 5000 -- m, if units are within this distance value, they are considered "close" AIEN.config.supportDistance = 8000 -- m, maximum distance for evaluating support or cover movements when under attack AIEN.config.withrawDist = 15000 -- m, maximum distance for withdraw manoeuvre nearby a friendly support unit end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- changes to CTLD ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Our CTLD (VEAF version) does not autoinitialize. It's also set to log messages using the VEAF logging functions -- Instead, we count on the mission makers to call ctld.initialize from the missionConfig.lua file (since v5.0) -- Here, we're upgrading the vanilla CTLD initialize function so it's smarter ---The VEAF replacement function that wraps up around ctld.initialize ---@param configurationCallback function? a callback that will be called before calling the vanilla ctld.initialize function function veaf.ctld_initialize_replacement(configurationCallback) if ctld then veaf.loggers.get(veaf.Id):info(string.format("Setting up CTLD")) -- logging change ctld.p = veaf.p ctld.Id = "CTLD" --ctld.LogLevel = "info" --ctld.LogLevel = "debug" --ctld.LogLevel = "trace" ctld.logger = veaf.loggers.new(ctld.Id, ctld.LogLevel) -- override the ctld logs with our own methods ---@diagnostic disable-next-line: duplicate-set-field ctld.logError = function(message, args) veaf.loggers.get(ctld.Id):error(message, args) end -- override the ctld logs with our own methods ---@diagnostic disable-next-line: duplicate-set-field ctld.logInfo = function(message, args) veaf.loggers.get(ctld.Id):info(message, args) end -- override the ctld logs with our own methods ---@diagnostic disable-next-line: duplicate-set-field ctld.logDebug = function(message, args) veaf.loggers.get(ctld.Id):debug(message, args) end -- override the ctld logs with our own methods ---@diagnostic disable-next-line: duplicate-set-field ctld.logTrace = function(message, args) veaf.loggers.get(ctld.Id):trace(message, args) end -- global configuration change ctld.addPlayerAircraftByType = true ctld.loadCrateFromMenu = true -- if set to true, you can load crates with the F10 menu OR hovering, in case of using choppers and planes for example. ctld.slingLoad = true -- if false, crates can be used WITHOUT slingloading, by hovering above the crate, simulating slingloading but not the weight... ctld.crateWaitTime = 0 -- time in seconds to wait before you can spawn another crate -- Simulated Sling load configuration ctld.minimumHoverHeight = 5.0 -- Lowest allowable height for crate hover ctld.maximumHoverHeight = 15.0 -- Highest allowable height for crate hover ctld.maxDistanceFromCrate = 8.0 -- Maximum distance from from crate for hover ctld.hoverTime = 10 -- Time to hold hover above a crate for loading in seconds -- ************** Maximum Units SETUP for UNITS ****************** ctld.unitLoadLimits["UH-1H"] = 10 ctld.unitLoadLimits["Mi-8MT"] = 20 ctld.unitLoadLimits["UH-60L"] = 20 ctld.unitLoadLimits["Yak-52"] = 1 ctld.unitLoadLimits["SA342L"] = 1 ctld.unitLoadLimits["SA342M"] = 1 ctld.unitLoadLimits["SA342Mistral"] = 1 ctld.unitLoadLimits["SA342Minigun"] = 1 ctld.unitLoadLimits["CH-47Fbl1"] = 33 ctld.internalCargoLimits["Mi-8MT"] = 2 ctld.internalCargoLimits["CH-47Fbl1"] = 4 -- ************** Allowable actions for UNIT TYPES ****************** -- Add VEAF-specific aircraft types to the existing table local veafAircraftTypes = { "Hercules", "UH-60L", "Ka-50", "Ka-50_3", "SA342L", "SA342M", "SA342Mistral", "SA342Minigun", "Yak-52", } for _, aircraftType in ipairs(veafAircraftTypes) do if not veaf.tableContains(ctld.aircraftTypeTable, aircraftType) then table.insert(ctld.aircraftTypeTable, aircraftType) end end ctld.unitActions["Yak-52"] = {crates=false, troops=true} ctld.unitActions["UH-60L"] = {crates=true, troops=true} ctld.unitActions["SA342L"] = {crates=false, troops=true} ctld.unitActions["SA342M"] = {crates=false, troops=true} ctld.unitActions["SA342Mistral"] = {crates=false, troops=true} ctld.unitActions["SA342Minigun"] = {crates=false, troops=true} -- ************** INFANTRY GROUPS FOR PICKUP ****************** ctld.autoInitializeAllLogistic = function() local LogisticTypeNames = {"LHA_Tarawa", "Stennis", "CVN_71", "KUZNECOW", "FARP Ammo Storage", "FARP Ammo Dump Coating"} veaf.loggers.get(ctld.Id):info("autoInitializeAllLogistic()") ctld.logisticUnits = {} local units = mist.DBs.unitsByName -- local copy for faster execution for name, unit in pairs(units) do veaf.loggers.get(ctld.Id):trace(string.format("name=%s, unit.type=%s", veaf.p(name), veaf.p(unit.type))) if unit then for _, unitTypeName in pairs(LogisticTypeNames) do if unitTypeName:lower() == unit.type:lower() then table.insert(ctld.logisticUnits, unit.unitName) veaf.loggers.get(ctld.Id):debug("Adding CTLD logistic unit %s of group %s", veaf.p(unit.unitName), veaf.p(unit.groupName)) end end end end -- generate 20 logistic unit names in the form "logistic #001" veaf.loggers.get(ctld.Id):debug("generate 20 logistic unit names in the form 'logistic #001'") for i = 1, 20 do table.insert(ctld.logisticUnits, string.format("logistic #%03d",i)) end veaf.loggers.get(ctld.Id):trace("ctld.logisticUnits=%s", veaf.p(ctld.logisticUnits)) end ctld.autoInitializeAllPickupZones = function() local PickupShipNames = {"LHA_Tarawa", "Stennis", "CVN_71", "KUZNECOW"} veaf.loggers.get(ctld.Id):info("autoInitializeAllPickupZones()") ctld.pickupZones = {} -- add all ships to the pickup zones table local units = mist.makeUnitTable({"[all][ship]"}) -- get all ships in the mission veaf.loggers.get(ctld.Id):trace("units=%s", veaf.p(units)) for _, unitName in pairs(units) do if unitName then local unitObject = Unit.getByName(unitName) local _unitCoalition = nil if unitObject then _unitCoalition = veaf.getCoalitionForCountry(veaf.getCountryName(unitObject:getCountry()), true) end local zone = {unitName, nil, -1, "yes", _unitCoalition, nil} table.insert(ctld.pickupZones, zone) veaf.loggers.get(ctld.Id):debug("Adding CTLD pickup zone for ship: [%s]", veaf.p(zone)) end end -- generate 20 pickup zone names in the form "pickzone #001" veaf.loggers.get(ctld.Id):debug("generate 20 pickup zone names in the form 'pickzone #001'") for i = 1, 20 do table.insert(ctld.pickupZones, { string.format("pickzone #%03d",i), "none", -1, "yes", 0 }) end veaf.loggers.get(ctld.Id):trace("ctld.pickupZones=%s", veaf.p(ctld.pickupZones)) end -- automatically add all the carriers and FARPs to ctld.logisticUnits ctld.autoInitializeAllLogistic() -- automatically generate pickup zones names ctld.autoInitializeAllPickupZones() -- if a callback is defined, this is the right moment to call it if configurationCallback and type(configurationCallback) == "function" then -- a configuration callback has been set, call it veaf.loggers.get(ctld.Id):info("calling the configuration callback") configurationCallback() veaf.loggers.get(ctld.Id):info("done calling the configuration callback") end -- call the actual CTLD.initialize veaf.ctld_initialize(true) veaf.ctld_initialized = true veaf.loggers.get(ctld.Id):info(string.format("Done setting up CTLD")) else veaf.loggers.get(veaf.Id):error(string.format("CTLD is not loaded")) end end if ctld then veaf.loggers.get(veaf.Id):info(string.format("replacing CTLD.initialize()")) veaf.ctld_initialize = ctld.initialize -- used to call the vanilla ctld.initialize from the VEAF replacement ctld.initialize = veaf.ctld_initialize_replacement -- replace the ctld.initialize with the VEAF wrapper function end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- changes to CSAR ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Our CSAR (VEAF version) does not autoinitialize. It's also set to log messages using the VEAF logging functions -- Instead, we count on the mission makers to call csar.initialize from the missionConfig.lua file (since v5.0) -- Here, we're upgrading the vanilla CSAR initialize function so it's smarter ---The VEAF replacement function that wraps up around ctld.initialize ---@param configurationCallback function? a callback that will be called before calling the vanilla csar.initialize function function veaf.csar_initialize_replacement(configurationCallback) if csar then veaf.loggers.get(veaf.Id):info(string.format("Setting up CSAR")) -- change the init function so we can call it whenever we want csar.skipInitialisation = true -- logging change csar.p = veaf.p csar.Id = "CSAR" --csar.LogLevel = "info" --csar.LogLevel = "trace" --csar.LogLevel = "debug" csar.logger = veaf.loggers.new(csar.Id, csar.LogLevel) -- override the csar logs with our own methods ---@diagnostic disable-next-line: duplicate-set-field csar.logError = function(message) veaf.loggers.get(csar.Id):error(message) end -- override the csar logs with our own methods ---@diagnostic disable-next-line: duplicate-set-field csar.logInfo = function(message) veaf.loggers.get(csar.Id):info(message) end -- override the csar logs with our own methods ---@diagnostic disable-next-line: duplicate-set-field csar.logDebug = function(message) veaf.loggers.get(csar.Id):debug(message) end -- override the csar logs with our own methods ---@diagnostic disable-next-line: duplicate-set-field csar.logTrace = function(message) veaf.loggers.get(csar.Id):trace(message) end -- global configuration change csar.enableAllslots = true csar.useprefix = false csar.radioSound = "csar-beacon.ogg" if configurationCallback and type(configurationCallback) == "function" then -- a configuration callback has been set, call it veaf.loggers.get(csar.Id):info("calling the configuration callback") configurationCallback() veaf.loggers.get(csar.Id):info("done calling the configuration callback") end -- call the actual CSAR.initialize veaf.csar_initialize(true) veaf.csar_initialized = true veaf.loggers.get(csar.Id):info(string.format("Done setting up CSAR")) else veaf.loggers.get(veaf.Id):error(string.format("CSAR is not loaded")) end end if csar then veaf.loggers.get(veaf.Id):info(string.format("replacing CSAR.initialize()")) veaf.csar_initialize = csar.initialize -- used to call the vanilla csar.initialize from the VEAF replacement csar.initialize = veaf.csar_initialize_replacement -- replace the csar.initialize with the VEAF wrapper function end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- changes to STTS ------------------------------------------------------------------------------------------------------------------------------------------------------------- if STTS then veaf.loggers.get(veaf.Id):info(string.format("Setting up STTS")) --- configure SRS Text to Speech veaf.loggers.get(veaf.Id):trace(string.format("STTS - SERVER_CONFIG=%s", veaf.p(SERVER_CONFIG))) if SERVER_CONFIG then veaf.loggers.get(veaf.Id):info(string.format("Setting up STTS")) STTS.DIRECTORY = SERVER_CONFIG.SRS_DIRECTORY STTS.SRS_PORT = SERVER_CONFIG.SRS_PORT STTS.EXECUTABLE = SERVER_CONFIG.SRS_EXECUTABLE STTS.os = SERVER_CONFIG.getModule("os") STTS.io = SERVER_CONFIG.getModule("io") veaf.loggers.get(veaf.Id):info(string.format("Done setting up STTS")) end end ------------------ END script veaf.lua ------------------ ------------------ START script dcsUnits.lua ------------------ ------------------------------------------------------------------ -- DCS World units database -- By zip (2018) -- -- Features: -- --------- -- * lists the DCS world units -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ dcsUnits = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the root VEAF constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. dcsUnits.Id = "DCSUNITS - " --- Version. dcsUnits.Version = "2025.11.16" -- trace level, specific to this module --dcsUnits.LogLevel = "trace" --dcsUnits.LogLevel = "debug" dcsUnits.logger = veaf.loggers.new(dcsUnits.Id, dcsUnits.LogLevel) ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- --manually filled out list of all Naval statics, perhaps could be automated but did not find any patterns dcsUnits.NavalStatics = { ["offshore WindTurbine"]=true, ["offshore WindTurbine2"]=true, ["Oil platform"]=true, ["Orca"]=true, ["Gas platform"]=true, ["Oil rig"]=true, ["M1 barrage balloon"] = true, } ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Raw DCS units database ------------------------------------------------------------------------------------------------------------------------------------------------------------- dcsUnits.DcsUnitsDatabase = { [1] = { ["type"] = "1L13 EWR", ["name"] = "EWR 1L13", ["category"] = "Air Defence", ["description"] = "EWR 1L13", ["vehicle"] = true, ["attribute"] = { [1] = true, [2] = true, ["Vehicles"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["Ground vehicles"] = true, ["Air Defence vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["EWR"] = true, [101] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [1] [2] = { ["type"] = "2S6 Tunguska", ["name"] = "SAM SA-19 Tunguska \"Grison\" ", ["category"] = "Air Defence", ["description"] = "SAM SA-19 Tunguska \"Grison\" ", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Rocket Attack Valid AirDefence"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["AAA"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["AA_missile"] = true, ["Mobile AAA"] = true, ["SAM related"] = true, ["Ground vehicles"] = true, ["NonArmoredUnits"] = true, ["Vehicles"] = true, ["Ground Units"] = true, ["Air Defence"] = true, ["All"] = true, ["SR SAM"] = true, ["NonAndLightArmoredUnits"] = true, [103] = true, ["SAM TR"] = true, ["Armed Air Defence"] = true, [29] = true, ["AA_flak"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "SA-19 Tunguska 2S6", }, -- end of ["aliases"] }, -- end of [2] [3] = { ["type"] = "55G6 EWR", ["name"] = "EWR 55G6", ["category"] = "Air Defence", ["description"] = "EWR 55G6", ["vehicle"] = true, ["attribute"] = { ["Vehicles"] = true, ["NonArmoredUnits"] = true, ["Air Defence vehicles"] = true, ["Air Defence"] = true, ["Ground vehicles"] = true, [101] = true, ["NonAndLightArmoredUnits"] = true, ["EWR"] = true, [2] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [3] [4] = { ["type"] = "5p73 s-125 ln", ["name"] = "SAM SA-3 S-125 \"Goa\" LN", ["category"] = "Air Defence", ["description"] = "SAM SA-3 S-125 \"Goa\" LN", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["AA_missile"] = true, [74] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["Armed Air Defence"] = true, [27] = true, ["SAM related"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["SAM LL"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [4] [5] = { ["type"] = "Allies_Director", ["name"] = "Allies Rangefinder (DRT)", ["category"] = "Air Defence", ["description"] = "Allies Rangefinder (DRT)", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, [2] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["Armed Air Defence"] = true, [330] = true, ["Air Defence"] = true, ["NonArmoredUnits"] = true, ["Static AAA"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [5] [6] = { ["type"] = "bofors40", ["name"] = "AAA Bofors 40mm", ["category"] = "Air Defence", ["description"] = "AAA Bofors 40mm", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, ["AAA"] = true, [16] = true, [47] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [6] [7] = { ["type"] = "CHAP_IRISTSLM_CP", ["name"] = "SAM IRIS-T SLM C2 [CH]", ["category"] = "Air Defence", ["description"] = "SAM IRIS-T SLM C2 [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["SAM CC"] = true, ["Ground Units Non Airdefence"] = true, ["NonArmoredUnits"] = true, [25] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["Unarmed vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [362] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [7] [8] = { ["type"] = "CHAP_IRISTSLM_LN", ["name"] = "SAM IRIS-T SLM LN [CH]", ["category"] = "Air Defence", ["description"] = "SAM IRIS-T SLM LN [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["AA_missile"] = true, ["NonArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["SAM related"] = true, [361] = true, ["SAM LL"] = true, [27] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [8] [9] = { ["type"] = "CHAP_IRISTSLM_STR", ["name"] = "SAM IRIS-T SLM STR [CH]", ["category"] = "Air Defence", ["description"] = "SAM IRIS-T SLM STR [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, [360] = true, ["Air Defence"] = true, ["SAM related"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, [101] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [9] [10] = { ["type"] = "CHAP_PantsirS1", ["name"] = "SAM SA-22 Pantsir-S1 \"Greyhound\" [CH]", ["category"] = "Air Defence", ["description"] = "SAM SA-22 Pantsir-S1 \"Greyhound\" [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Rocket Attack Valid AirDefence"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["AAA"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["AA_missile"] = true, ["Mobile AAA"] = true, ["SAM related"] = true, ["Ground vehicles"] = true, ["Vehicles"] = true, ["NonArmoredUnits"] = true, ["Ground Units"] = true, ["All"] = true, ["Air Defence"] = true, [103] = true, ["SR SAM"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, ["AA_flak"] = true, ["Armed Air Defence"] = true, ["Datalink"] = true, [355] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [10] [11] = { ["type"] = "CHAP_TorM2", ["name"] = "SAM SA-15 Tor M2 \"Gauntlet\" [CH]", ["category"] = "Air Defence", ["description"] = "SAM SA-15 Tor M2 \"Gauntlet\" [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, [363] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["AA_missile"] = true, ["NonArmoredUnits"] = true, ["SAM related"] = true, ["Air Defence"] = true, ["SAM TR"] = true, [102] = true, ["NonAndLightArmoredUnits"] = true, ["Vehicles"] = true, ["Ground Units"] = true, ["All"] = true, ["Datalink"] = true, ["SR SAM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [11] [12] = { ["type"] = "Dog Ear radar", ["name"] = "MCC-SR Sborka \"Dog Ear\" SR", ["category"] = "Air Defence", ["description"] = "MCC-SR Sborka \"Dog Ear\" SR", ["vehicle"] = true, ["attribute"] = { ["SAM elements"] = true, ["SAM related"] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["Air Defence"] = true, ["SAM SR"] = true, [101] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [12] [13] = { ["type"] = "flak18", ["name"] = "AAA 8,8cm Flak 18", ["category"] = "Air Defence", ["description"] = "AAA 8,8cm Flak 18", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, [314] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [13] [14] = { ["type"] = "flak30", ["name"] = "AAA Flak 38 20mm", ["category"] = "Air Defence", ["description"] = "AAA Flak 38 20mm", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, [280] = true, ["All"] = true, [16] = true, ["AAA"] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [14] [15] = { ["type"] = "flak36", ["name"] = "AAA 8,8cm Flak 36", ["category"] = "Air Defence", ["description"] = "AAA 8,8cm Flak 36", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, ["AAA"] = true, [16] = true, [47] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [15] [16] = { ["type"] = "flak37", ["name"] = "AAA 8,8cm Flak 37", ["category"] = "Air Defence", ["description"] = "AAA 8,8cm Flak 37", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, ["AAA"] = true, [16] = true, [47] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [16] [17] = { ["type"] = "flak38", ["name"] = "AAA Flak-Vierling 38 Quad 20mm", ["category"] = "Air Defence", ["description"] = "AAA Flak-Vierling 38 Quad 20mm", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, [281] = true, [16] = true, ["AAA"] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [17] [18] = { ["type"] = "flak41", ["name"] = "AAA 8,8cm Flak 41", ["category"] = "Air Defence", ["description"] = "AAA 8,8cm Flak 41", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, ["AAA"] = true, [16] = true, [47] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [18] [19] = { ["type"] = "Flakscheinwerfer_37", ["name"] = "SL Flakscheinwerfer 37", ["category"] = "Air Defence", ["description"] = "SL Flakscheinwerfer 37", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, [26] = true, ["Vehicles"] = true, ["NonArmoredUnits"] = true, [2] = true, ["All"] = true, ["Air Defence"] = true, ["AAA"] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, [282] = true, ["Static AAA"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [19] [20] = { ["type"] = "FPS-117", ["name"] = "EWR AN/FPS-117 Radar", ["category"] = "Air Defence", ["description"] = "EWR AN/FPS-117 Radar", ["vehicle"] = true, ["attribute"] = { ["Vehicles"] = true, ["NonArmoredUnits"] = true, ["Air Defence vehicles"] = true, [2] = true, ["Air Defence"] = true, ["Ground vehicles"] = true, [329] = true, ["NonAndLightArmoredUnits"] = true, ["EWR"] = true, [101] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [20] [21] = { ["type"] = "FPS-117 Dome", ["name"] = "EWR AN/FPS-117 Radar (domed)", ["category"] = "Air Defence", ["description"] = "EWR AN/FPS-117 Radar (domed)", ["vehicle"] = true, ["attribute"] = { ["Vehicles"] = true, ["NonArmoredUnits"] = true, ["Air Defence vehicles"] = true, [327] = true, ["Air Defence"] = true, ["Ground vehicles"] = true, [2] = true, ["NonAndLightArmoredUnits"] = true, ["EWR"] = true, [101] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [21] [22] = { ["type"] = "FPS-117 ECS", ["name"] = "EWR AN/FPS-117 ECS", ["category"] = "Air Defence", ["description"] = "EWR AN/FPS-117 ECS", ["vehicle"] = true, ["attribute"] = { ["Vehicles"] = true, ["SAM elements"] = true, [2] = true, ["SAM related"] = true, ["Air Defence"] = true, [16] = true, [328] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, [101] = true, ["All"] = true, ["Ground Units"] = true, ["SAM CC"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [22] [23] = { ["type"] = "FuMG-401", ["name"] = "EWR FuMG-401 Freya LZ", ["category"] = "Air Defence", ["description"] = "EWR FuMG-401 Freya LZ", ["vehicle"] = true, ["attribute"] = { ["Vehicles"] = true, ["NonArmoredUnits"] = true, ["Air Defence vehicles"] = true, [2] = true, ["Air Defence"] = true, ["Ground vehicles"] = true, [284] = true, ["NonAndLightArmoredUnits"] = true, ["EWR"] = true, [101] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [23] [24] = { ["type"] = "FuSe-65", ["name"] = "EWR FuSe-65 Würzburg-Riese", ["category"] = "Air Defence", ["description"] = "EWR FuSe-65 Würzburg-Riese", ["vehicle"] = true, ["attribute"] = { ["Vehicles"] = true, ["NonArmoredUnits"] = true, ["Air Defence vehicles"] = true, [285] = true, ["Air Defence"] = true, ["Ground vehicles"] = true, [2] = true, ["NonAndLightArmoredUnits"] = true, ["EWR"] = true, [101] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [24] [25] = { ["type"] = "generator_5i57", ["name"] = "Diesel Power Station 5I57A", ["category"] = "Air Defence", ["description"] = "Diesel Power Station 5I57A", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, ["AD Auxillary Equipment"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, [9] = true, ["Air Defence vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, [293] = true, ["All"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [25] [26] = { ["type"] = "Gepard", ["name"] = "SPAAA Gepard", ["category"] = "Air Defence", ["description"] = "SPAAA Gepard", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Rocket Attack Valid AirDefence"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["AAA"] = true, ["RADAR_BAND1_FOR_ARM"] = true, [38] = true, ["Mobile AAA"] = true, ["NonArmoredUnits"] = true, ["SAM related"] = true, ["Ground vehicles"] = true, ["Air Defence"] = true, ["Vehicles"] = true, ["AA_flak"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["SAM TR"] = true, ["Armed Air Defence"] = true, ["Ground Units"] = true, [105] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [26] [27] = { ["type"] = "Hawk cwar", ["name"] = "SAM Hawk CWAR AN/MPQ-55", ["category"] = "Air Defence", ["description"] = "SAM Hawk CWAR AN/MPQ-55", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, [42] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["Ground Units"] = true, ["All"] = true, ["Datalink"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [27] [28] = { ["type"] = "Hawk ln", ["name"] = "SAM Hawk LN M192", ["category"] = "Air Defence", ["description"] = "SAM Hawk LN M192", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["AA_missile"] = true, [41] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["Armed Air Defence"] = true, ["SAM related"] = true, ["NonAndLightArmoredUnits"] = true, [27] = true, ["SAM LL"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "Hawk M192 LN", }, -- end of ["aliases"] }, -- end of [28] [29] = { ["type"] = "Hawk pcp", ["name"] = "SAM Hawk Platoon Command Post (PCP)", ["category"] = "Air Defence", ["description"] = "SAM Hawk Platoon Command Post (PCP)", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, ["SAM elements"] = true, [2] = true, ["SAM related"] = true, [16] = true, ["Ground vehicles"] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, [6] = true, ["All"] = true, ["Ground Units"] = true, ["SAM CC"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [29] [30] = { ["type"] = "Hawk sr", ["name"] = "SAM Hawk SR (AN/MPQ-50)", ["category"] = "Air Defence", ["description"] = "SAM Hawk SR (AN/MPQ-50)", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["SAM related"] = true, ["NonAndLightArmoredUnits"] = true, [39] = true, ["Ground Units"] = true, ["All"] = true, ["Datalink"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "Hawk AN/MPQ-50 SR", }, -- end of ["aliases"] }, -- end of [30] [31] = { ["type"] = "Hawk tr", ["name"] = "SAM Hawk TR (AN/MPQ-46)", ["category"] = "Air Defence", ["description"] = "SAM Hawk TR (AN/MPQ-46)", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, [40] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["SAM TR"] = true, ["All"] = true, ["Ground Units"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "Hawk AN/MPQ-46 TR", }, -- end of ["aliases"] }, -- end of [31] [32] = { ["type"] = "HEMTT_C-RAM_Phalanx", ["name"] = "LPWS C-RAM", ["category"] = "Air Defence", ["description"] = "LPWS C-RAM", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["C-RAM"] = true, ["Ground vehicles"] = true, [105] = true, ["Mobile AAA"] = true, ["AAA"] = true, ["NonArmoredUnits"] = true, ["Rocket Attack Valid AirDefence"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["SAM related"] = true, ["SAM TR"] = true, ["NonAndLightArmoredUnits"] = true, ["AA_flak"] = true, ["Ground Units"] = true, ["All"] = true, ["Datalink"] = true, [342] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [32] [33] = { ["type"] = "HL_ZU-23", ["name"] = "SPAAA HL with ZU-23", ["category"] = "Air Defence", ["description"] = "SPAAA HL with ZU-23", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["Ground Units"] = true, ["AA_flak"] = true, [325] = true, ["All"] = true, [16] = true, ["AAA"] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, [2] = true, ["Air Defence"] = true, ["Armed Air Defence"] = true, ["Mobile AAA"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [33] [34] = { ["type"] = "HQ-7_LN_P", ["name"] = "HQ-7 SHORAD TELAR (Player)", ["category"] = "Air Defence", ["description"] = "HQ-7 SHORAD TELAR (Player)", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["AA_missile"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["NonArmoredUnits"] = true, ["Armed Air Defence"] = true, ["SAM related"] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, [102] = true, ["SAM LL"] = true, [346] = true, ["SAM TR"] = true, ["All"] = true, ["Ground Units"] = true, ["SR SAM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [34] [35] = { ["type"] = "HQ-7_LN_SP", ["name"] = "HQ-7B SHORAD TELAR", ["category"] = "Air Defence", ["description"] = "HQ-7B SHORAD TELAR", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["AA_missile"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["NonArmoredUnits"] = true, ["Armed Air Defence"] = true, ["SAM related"] = true, ["Air Defence"] = true, ["SAM TR"] = true, [102] = true, ["SAM LL"] = true, [277] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["SR SAM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [35] [36] = { ["type"] = "HQ-7_STR_SP", ["name"] = "HQ-7B SHORAD SR", ["category"] = "Air Defence", ["description"] = "HQ-7B SHORAD SR", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, [278] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["SAM CC"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["SR SAM"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM elements"] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [36] [37] = { ["infantry"] = true, ["type"] = "Igla manpad INS", ["name"] = "MANPADS SA-18 Igla \"Grouse\" Ins", ["category"] = "Air Defence", ["description"] = "MANPADS SA-18 Igla \"Grouse\" Ins", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, [62] = true, ["SAM related"] = true, ["New infantry"] = true, ["Armed ground units"] = true, ["MANPADS"] = true, ["IR Guided SAM"] = true, ["SAM"] = true, ["NonArmoredUnits"] = true, ["Rocket Attack Valid AirDefence"] = true, ["Infantry"] = true, ["Air Defence"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, [27] = true, ["Armed Air Defence"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [37] [38] = { ["type"] = "KDO_Mod40", ["name"] = "AAA Kdo.G.40", ["category"] = "Air Defence", ["description"] = "AAA Kdo.G.40", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["Armed Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, [316] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [38] [39] = { ["type"] = "KS-19", ["name"] = "AAA KS-19 100mm", ["category"] = "Air Defence", ["description"] = "AAA KS-19 100mm", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["Ground Units"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["Armed Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["Air Defence"] = true, ["NonArmoredUnits"] = true, ["Static AAA"] = true, [334] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [39] [40] = { ["type"] = "Kub 1S91 str", ["name"] = "SAM SA-6 Kub \"Straight Flush\" STR", ["category"] = "Air Defence", ["description"] = "SAM SA-6 Kub \"Straight Flush\" STR", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, [21] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["SAM TR"] = true, ["All"] = true, ["Ground Units"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "SA-6 Kub STR 9S91", }, -- end of ["aliases"] }, -- end of [40] [41] = { ["type"] = "Kub 2P25 ln", ["name"] = "SAM SA-6 Kub \"Gainful\" TEL", ["category"] = "Air Defence", ["description"] = "SAM SA-6 Kub \"Gainful\" TEL", ["vehicle"] = true, ["attribute"] = { ["Armed Air Defence"] = true, ["SAM elements"] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["SAM related"] = true, ["Air Defence"] = true, [16] = true, ["Ground vehicles"] = true, ["NonArmoredUnits"] = true, ["SAM LL"] = true, ["NonAndLightArmoredUnits"] = true, ["AA_missile"] = true, ["All"] = true, ["Ground Units"] = true, [22] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "SA-6 Kub LN 2P25", }, -- end of ["aliases"] }, -- end of [41] [42] = { ["type"] = "M1097 Avenger", ["name"] = "SAM Avenger (Stinger)", ["category"] = "Air Defence", ["description"] = "SAM Avenger (Stinger)", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["AA_flak"] = true, [33] = true, ["SAM related"] = true, ["AA_missile"] = true, ["IR Guided SAM"] = true, ["SAM"] = true, ["NonArmoredUnits"] = true, ["Ground vehicles"] = true, ["Air Defence"] = true, ["Ground Units"] = true, ["SR SAM"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, [104] = true, ["Armed Air Defence"] = true, ["Datalink"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [42] [43] = { ["type"] = "M1_37mm", ["name"] = "AAA M1 37mm", ["category"] = "Air Defence", ["description"] = "AAA M1 37mm", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, [2] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["Armed Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["Air Defence"] = true, ["NonArmoredUnits"] = true, [288] = true, ["Ground Units"] = true, ["Static AAA"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [43] [44] = { ["type"] = "M45_Quadmount", ["name"] = "AAA M45 Quadmount HB 12.7mm", ["category"] = "Air Defence", ["description"] = "AAA M45 Quadmount HB 12.7mm", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["Armed Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["Air Defence"] = true, [287] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [44] [45] = { ["type"] = "M48 Chaparral", ["name"] = "SAM Chaparral M48", ["category"] = "Air Defence", ["description"] = "SAM Chaparral M48", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, [16] = true, ["SAM related"] = true, ["AA_missile"] = true, ["IR Guided SAM"] = true, ["SAM"] = true, ["NonArmoredUnits"] = true, ["Ground vehicles"] = true, ["Air Defence"] = true, ["Ground Units"] = true, ["SR SAM"] = true, ["NonAndLightArmoredUnits"] = true, [50] = true, [27] = true, ["Armed Air Defence"] = true, ["Datalink"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [45] [46] = { ["type"] = "M6 Linebacker", ["name"] = "SAM Linebacker - Bradley M6", ["category"] = "Air Defence", ["description"] = "SAM Linebacker - Bradley M6", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["AA_flak"] = true, ["SAM related"] = true, ["AA_missile"] = true, ["IR Guided SAM"] = true, ["SAM"] = true, ["NonArmoredUnits"] = true, ["Ground vehicles"] = true, ["Ground Units"] = true, ["Air Defence"] = true, [51] = true, ["SR SAM"] = true, [104] = true, ["All"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Datalink"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [46] [47] = { ["type"] = "Maschinensatz_33", ["name"] = "Maschinensatz 33 Gen", ["category"] = "Air Defence", ["description"] = "Maschinensatz 33 Gen", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, ["AD Auxillary Equipment"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, [9] = true, ["Air Defence vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, [283] = true, ["All"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [47] [48] = { ["type"] = "NASAMS_Command_Post", ["name"] = "SAM NASAMS C2", ["category"] = "Air Defence", ["description"] = "SAM NASAMS C2", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["SAM CC"] = true, ["Ground Units Non Airdefence"] = true, ["NonArmoredUnits"] = true, [306] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, [25] = true, ["All"] = true, ["Ground Units"] = true, ["Unarmed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [48] [49] = { ["type"] = "NASAMS_LN_B", ["name"] = "SAM NASAMS LN AIM-120B", ["category"] = "Air Defence", ["description"] = "SAM NASAMS LN AIM-120B", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["AA_missile"] = true, ["NonArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["SAM related"] = true, [307] = true, ["SAM LL"] = true, [27] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [49] [50] = { ["type"] = "NASAMS_LN_C", ["name"] = "SAM NASAMS LN AIM-120C", ["category"] = "Air Defence", ["description"] = "SAM NASAMS LN AIM-120C", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["AA_missile"] = true, ["NonArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["SAM related"] = true, [308] = true, [27] = true, ["NonAndLightArmoredUnits"] = true, ["SAM LL"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [50] [51] = { ["type"] = "NASAMS_Radar_MPQ64F1", ["name"] = "SAM NASAMS SR MPQ64F1", ["category"] = "Air Defence", ["description"] = "SAM NASAMS SR MPQ64F1", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, [305] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, [101] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [51] [52] = { ["type"] = "Osa 9A33 ln", ["name"] = "SAM SA-8 Osa \"Gecko\" TEL", ["category"] = "Air Defence", ["description"] = "SAM SA-8 Osa \"Gecko\" TEL", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["AA_missile"] = true, ["RADAR_BAND2_FOR_ARM"] = true, [23] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, [102] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["SAM TR"] = true, ["All"] = true, ["Ground Units"] = true, ["SR SAM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [52] [53] = { ["type"] = "p-19 s-125 sr", ["name"] = "SAM SA-2/3/5 P19 \"Flat Face\" SR ", ["category"] = "Air Defence", ["description"] = "SAM SA-2/3/5 P19 \"Flat Face\" SR ", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, [75] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [53] [54] = { ["type"] = "Patriot AMG", ["name"] = "SAM Patriot CR (AMG AN/MRC-137)", ["category"] = "Air Defence", ["description"] = "SAM Patriot CR (AMG AN/MRC-137)", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [17] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [36] = true, ["SAM CC"] = true, ["Ground Units Non Airdefence"] = true, ["NonArmoredUnits"] = true, [25] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, ["Unarmed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [54] [55] = { ["type"] = "Patriot cp", ["name"] = "SAM Patriot C2 ICC", ["category"] = "Air Defence", ["description"] = "SAM Patriot C2 ICC", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [17] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [36] = true, ["SAM CC"] = true, ["Ground Units Non Airdefence"] = true, ["NonArmoredUnits"] = true, [25] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, ["Unarmed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [55] [56] = { ["type"] = "Patriot ECS", ["name"] = "SAM Patriot ECS", ["category"] = "Air Defence", ["description"] = "SAM Patriot ECS", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [36] = true, ["SAM CC"] = true, ["Ground Units Non Airdefence"] = true, ["NonArmoredUnits"] = true, [25] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, ["Unarmed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [56] [57] = { ["type"] = "Patriot EPP", ["name"] = "SAM Patriot EPP-III", ["category"] = "Air Defence", ["description"] = "SAM Patriot EPP-III", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [17] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [36] = true, ["SAM CC"] = true, ["Ground Units Non Airdefence"] = true, ["NonArmoredUnits"] = true, [25] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, ["Unarmed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [57] [58] = { ["type"] = "Patriot ln", ["name"] = "SAM Patriot LN", ["category"] = "Air Defence", ["description"] = "SAM Patriot LN", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["AA_missile"] = true, ["NonArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["SAM related"] = true, [37] = true, [27] = true, ["NonAndLightArmoredUnits"] = true, ["SAM LL"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [58] [59] = { ["type"] = "Patriot str", ["name"] = "SAM Patriot STR", ["category"] = "Air Defence", ["description"] = "SAM Patriot STR", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, [34] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["LR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["SAM related"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, [101] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [59] [60] = { ["type"] = "QF_37_AA", ["name"] = "AAA QF 3.7\"", ["category"] = "Air Defence", ["description"] = "AAA QF 3.7\"", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["Armed Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, [286] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [60] [61] = { ["type"] = "rapier_fsa_blindfire_radar", ["name"] = "SAM Rapier Blindfire TR", ["category"] = "Air Defence", ["description"] = "SAM Rapier Blindfire TR", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["SAM related"] = true, ["SR SAM"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, [262] = true, ["All"] = true, ["Ground Units"] = true, [27] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [61] [62] = { ["type"] = "rapier_fsa_launcher", ["name"] = "SAM Rapier LN", ["category"] = "Air Defence", ["description"] = "SAM Rapier LN", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["AA_missile"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["NonArmoredUnits"] = true, ["Armed Air Defence"] = true, ["SAM related"] = true, ["Air Defence"] = true, [260] = true, ["SR SAM"] = true, ["SAM LL"] = true, ["SAM TR"] = true, [27] = true, ["All"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [62] [63] = { ["type"] = "rapier_fsa_optical_tracker_unit", ["name"] = "SAM Rapier Tracker", ["category"] = "Air Defence", ["description"] = "SAM Rapier Tracker", ["vehicle"] = true, ["attribute"] = { [261] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["SAM elements"] = true, ["SAM SR"] = true, [16] = true, ["Ground vehicles"] = true, ["SR SAM"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["Air Defence"] = true, ["All"] = true, ["Ground Units"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [63] [64] = { ["type"] = "RD_75", ["name"] = "SAM SA-2 S-75 RD-75 Amazonka RF", ["category"] = "Air Defence", ["description"] = "SAM SA-2 S-75 RD-75 Amazonka RF", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, [337] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["SAM TR"] = true, ["All"] = true, ["Ground Units"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [64] [65] = { ["type"] = "RLS_19J6", ["name"] = "SAM SA-5 S-200 ST-68U \"Tin Shield\" SR", ["category"] = "Air Defence", ["description"] = "SAM SA-5 S-200 ST-68U \"Tin Shield\" SR", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, [309] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["LR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM elements"] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [65] [66] = { ["type"] = "Roland ADS", ["name"] = "SAM Roland ADS", ["category"] = "Air Defence", ["description"] = "SAM Roland ADS", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, [31] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["AA_missile"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["NonArmoredUnits"] = true, ["Armed Air Defence"] = true, ["SAM elements"] = true, ["Air Defence"] = true, ["SAM related"] = true, [102] = true, ["SAM LL"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, ["All"] = true, ["Ground Units"] = true, ["SR SAM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [66] [67] = { ["type"] = "Roland Radar", ["name"] = "SAM Roland EWR", ["category"] = "Air Defence", ["description"] = "SAM Roland EWR", ["vehicle"] = true, ["attribute"] = { ["Air Defence"] = true, ["SAM related"] = true, ["Vehicles"] = true, ["SAM elements"] = true, [2] = true, ["NonArmoredUnits"] = true, ["SAM SR"] = true, [101] = true, [32] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [67] [68] = { ["type"] = "RPC_5N62V", ["name"] = "SAM SA-5 S-200 \"Square Pair\" TR", ["category"] = "Air Defence", ["description"] = "SAM SA-5 S-200 \"Square Pair\" TR", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["LR SAM"] = true, [313] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [68] [69] = { ["type"] = "S-200_Launcher", ["name"] = "SAM SA-5 S-200 \"Gammon\" LN", ["category"] = "Air Defence", ["description"] = "SAM SA-5 S-200 \"Gammon\" LN", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["AA_missile"] = true, [381] = true, ["LR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["Armed Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, [27] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, ["SAM LL"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [69] [70] = { ["type"] = "S-300PS 40B6M tr", ["name"] = "SAM SA-10 S-300 \"Grumble\" Flap Lid-A TR", ["category"] = "Air Defence", ["description"] = "SAM SA-10 S-300 \"Grumble\" Flap Lid-A TR", ["vehicle"] = true, ["attribute"] = { ["LR SAM"] = true, ["SAM TR"] = true, ["Vehicles"] = true, ["SAM elements"] = true, [2] = true, [4] = true, ["SAM related"] = true, [101] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["NonAndLightArmoredUnits"] = true, ["Air Defence"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [70] [71] = { ["type"] = "S-300PS 40B6MD sr", ["name"] = "SAM SA-10 S-300 \"Grumble\" Clam Shell SR", ["category"] = "Air Defence", ["description"] = "SAM SA-10 S-300 \"Grumble\" Clam Shell SR", ["vehicle"] = true, ["attribute"] = { ["LR SAM"] = true, ["SAM related"] = true, ["Vehicles"] = true, ["SAM elements"] = true, [2] = true, ["Air Defence"] = true, ["SAM SR"] = true, [101] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, [5] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [71] [72] = { ["type"] = "S-300PS 40B6MD sr_19J6", ["name"] = "SAM SA-10 S-300 \"Grumble\" Tin Shield SR", ["category"] = "Air Defence", ["description"] = "SAM SA-10 S-300 \"Grumble\" Tin Shield SR", ["vehicle"] = true, ["attribute"] = { ["LR SAM"] = true, ["SAM related"] = true, ["Vehicles"] = true, ["SAM elements"] = true, [2] = true, ["Air Defence"] = true, ["SAM SR"] = true, [101] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, [345] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [72] [73] = { ["type"] = "S-300PS 54K6 cp", ["name"] = "SAM SA-10 S-300 \"Grumble\" C2", ["category"] = "Air Defence", ["description"] = "SAM SA-10 S-300 \"Grumble\" C2", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, ["SAM elements"] = true, [2] = true, ["SAM related"] = true, [16] = true, ["Ground vehicles"] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, [6] = true, ["All"] = true, ["Ground Units"] = true, ["SAM CC"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [73] [74] = { ["type"] = "S-300PS 5H63C 30H6_tr", ["name"] = "SAM SA-10 S-300 \"Grumble\" Flap Lid-B TR", ["category"] = "Air Defence", ["description"] = "SAM SA-10 S-300 \"Grumble\" Flap Lid-B TR", ["vehicle"] = true, ["attribute"] = { ["LR SAM"] = true, ["SAM TR"] = true, ["Vehicles"] = true, ["SAM elements"] = true, [2] = true, ["SAM related"] = true, ["Air Defence"] = true, [101] = true, ["Ground vehicles"] = true, [344] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [74] [75] = { ["type"] = "S-300PS 5P85C ln", ["name"] = "SAM SA-10 S-300 \"Grumble\" TEL C", ["category"] = "Air Defence", ["description"] = "SAM SA-10 S-300 \"Grumble\" TEL C", ["vehicle"] = true, ["attribute"] = { ["Armed Air Defence"] = true, ["SAM elements"] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["SAM related"] = true, [8] = true, [16] = true, ["Ground vehicles"] = true, ["Air Defence"] = true, ["AA_missile"] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["SAM LL"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [75] [76] = { ["type"] = "S-300PS 5P85D ln", ["name"] = "SAM SA-10 S-300 \"Grumble\" TEL D", ["category"] = "Air Defence", ["description"] = "SAM SA-10 S-300 \"Grumble\" TEL D", ["vehicle"] = true, ["attribute"] = { ["Armed Air Defence"] = true, ["SAM elements"] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["SAM related"] = true, ["Air Defence"] = true, [16] = true, [9] = true, ["NonArmoredUnits"] = true, ["SAM LL"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["AA_missile"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [76] [77] = { ["type"] = "S-300PS 64H6E sr", ["name"] = "SAM SA-10 S-300 \"Grumble\" Big Bird SR", ["category"] = "Air Defence", ["description"] = "SAM SA-10 S-300 \"Grumble\" Big Bird SR", ["vehicle"] = true, ["attribute"] = { ["LR SAM"] = true, ["SAM related"] = true, [7] = true, ["SAM elements"] = true, [2] = true, ["Air Defence"] = true, ["SAM SR"] = true, [16] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [77] [78] = { ["type"] = "S-60_Type59_Artillery", ["name"] = "AAA S-60 57mm", ["category"] = "Air Defence", ["description"] = "AAA S-60 57mm", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["Ground Units"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["Armed Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["Air Defence"] = true, ["NonArmoredUnits"] = true, ["Static AAA"] = true, [259] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [78] [79] = { ["type"] = "S_75M_Volhov", ["name"] = "SAM SA-2 S-75 \"Guideline\" LN", ["category"] = "Air Defence", ["description"] = "SAM SA-2 S-75 \"Guideline\" LN", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["AA_missile"] = true, ["LR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["Armed Air Defence"] = true, [380] = true, ["NonAndLightArmoredUnits"] = true, [27] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, ["SAM LL"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [79] [80] = { ["type"] = "SA-11 Buk CC 9S470M1", ["name"] = "SAM SA-11 Buk \"Gadfly\" C2 ", ["category"] = "Air Defence", ["description"] = "SAM SA-11 Buk \"Gadfly\" C2 ", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, ["SAM elements"] = true, [2] = true, ["SAM related"] = true, [16] = true, [17] = true, ["Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["SAM CC"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [80] [81] = { ["type"] = "SA-11 Buk LN 9A310M1", ["name"] = "SAM SA-11 Buk \"Gadfly\" Fire Dome TEL", ["category"] = "Air Defence", ["description"] = "SAM SA-11 Buk \"Gadfly\" Fire Dome TEL", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["AA_missile"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["SAM related"] = true, [102] = true, ["SAM LL"] = true, ["SAM TR"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, [19] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [81] [82] = { ["type"] = "SA-11 Buk SR 9S18M1", ["name"] = "SAM SA-11 Buk \"Gadfly\" Snow Drift SR", ["category"] = "Air Defence", ["description"] = "SAM SA-11 Buk \"Gadfly\" Snow Drift SR", ["vehicle"] = true, ["attribute"] = { ["SAM elements"] = true, ["SAM related"] = true, ["Vehicles"] = true, ["MR SAM"] = true, [2] = true, ["Air Defence"] = true, ["SAM SR"] = true, [101] = true, ["Ground vehicles"] = true, [18] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [82] [83] = { ["description"] = "MANPADS SA-18 Igla \"Grouse\" C2", ["type"] = "SA-18 Igla comm", ["name"] = "MANPADS SA-18 Igla \"Grouse\" C2", ["category"] = "Air Defence", ["infantry"] = true, ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["MANPADS AUX"] = true, ["Infantry"] = true, ["NonArmoredUnits"] = true, ["SAM AUX"] = true, ["Air Defence"] = true, ["SAM related"] = true, ["Ground Units Non Airdefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Rocket Attack Valid AirDefence"] = true, [55] = true, ["All"] = true, ["Ground Units"] = true, [27] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [83] [84] = { ["infantry"] = true, ["type"] = "SA-18 Igla manpad", ["name"] = "MANPADS SA-18 Igla \"Grouse\"", ["category"] = "Air Defence", ["description"] = "MANPADS SA-18 Igla \"Grouse\"", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, [16] = true, ["SAM related"] = true, ["New infantry"] = true, ["Armed ground units"] = true, ["MANPADS"] = true, ["IR Guided SAM"] = true, ["SAM"] = true, ["NonArmoredUnits"] = true, ["Rocket Attack Valid AirDefence"] = true, ["Infantry"] = true, ["Air Defence"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, [54] = true, [27] = true, ["Armed Air Defence"] = true, ["Ground Units"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [84] [85] = { ["description"] = "MANPADS SA-18 Igla-S \"Grouse\" C2", ["type"] = "SA-18 Igla-S comm", ["name"] = "MANPADS SA-18 Igla-S \"Grouse\" C2", ["category"] = "Air Defence", ["infantry"] = true, ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["MANPADS AUX"] = true, ["Infantry"] = true, ["NonArmoredUnits"] = true, ["SAM AUX"] = true, ["Air Defence"] = true, ["SAM related"] = true, ["Ground Units Non Airdefence"] = true, [53] = true, ["Rocket Attack Valid AirDefence"] = true, [27] = true, ["All"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [85] [86] = { ["infantry"] = true, ["type"] = "SA-18 Igla-S manpad", ["name"] = "MANPADS SA-18 Igla-S \"Grouse\"", ["category"] = "Air Defence", ["description"] = "MANPADS SA-18 Igla-S \"Grouse\"", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, [16] = true, ["SAM related"] = true, ["New infantry"] = true, ["Armed ground units"] = true, ["MANPADS"] = true, ["IR Guided SAM"] = true, ["SAM"] = true, ["NonArmoredUnits"] = true, ["Rocket Attack Valid AirDefence"] = true, ["Infantry"] = true, ["Air Defence"] = true, ["Ground Units Non Airdefence"] = true, [52] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Armed Air Defence"] = true, ["Ground Units"] = true, [27] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [86] [87] = { ["type"] = "snr s-125 tr", ["name"] = "SAM SA-3 S-125 \"Low Blow\" TR", ["category"] = "Air Defence", ["description"] = "SAM SA-3 S-125 \"Low Blow\" TR", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, [73] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, ["SAM related"] = true, ["All"] = true, ["Ground Units"] = true, [101] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [87] [88] = { ["type"] = "SNR_75V", ["name"] = "SAM SA-2 S-75 \"Fan Song\" TR", ["category"] = "Air Defence", ["description"] = "SAM SA-2 S-75 \"Fan Song\" TR", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, [256] = true, [101] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["MR SAM"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["SAM related"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["RADAR_BAND1_FOR_ARM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [88] [89] = { ["infantry"] = true, ["type"] = "Soldier stinger", ["name"] = "MANPADS Stinger", ["category"] = "Air Defence", ["description"] = "MANPADS Stinger", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, [16] = true, ["SAM related"] = true, ["New infantry"] = true, ["Armed ground units"] = true, ["MANPADS"] = true, ["IR Guided SAM"] = true, ["SAM"] = true, ["NonArmoredUnits"] = true, ["Rocket Attack Valid AirDefence"] = true, ["Infantry"] = true, ["Air Defence"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, [56] = true, [27] = true, ["Armed Air Defence"] = true, ["Ground Units"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [89] [90] = { ["type"] = "SON_9", ["name"] = "AAA Fire Can SON-9", ["category"] = "Air Defence", ["description"] = "AAA Fire Can SON-9", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, [335] = true, ["NonArmoredUnits"] = true, ["Rocket Attack Valid AirDefence"] = true, ["Air Defence"] = true, ["Armed Air Defence"] = true, ["SAM related"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, [101] = true, ["All"] = true, ["Ground Units"] = true, ["AAA"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [90] [91] = { ["description"] = "MANPADS Stinger C2", ["type"] = "Stinger comm", ["name"] = "MANPADS Stinger C2", ["category"] = "Air Defence", ["infantry"] = true, ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["MANPADS AUX"] = true, ["Infantry"] = true, ["NonArmoredUnits"] = true, ["SAM AUX"] = true, ["Air Defence"] = true, ["SAM related"] = true, ["Ground Units Non Airdefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Rocket Attack Valid AirDefence"] = true, [27] = true, ["All"] = true, [57] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [91] [92] = { ["description"] = "MANPADS Stinger C2 Desert", ["type"] = "Stinger comm dsr", ["name"] = "MANPADS Stinger C2 Desert", ["category"] = "Air Defence", ["infantry"] = true, ["vehicle"] = true, ["attribute"] = { [59] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["MANPADS AUX"] = true, ["Infantry"] = true, ["NonArmoredUnits"] = true, ["SAM AUX"] = true, ["Air Defence"] = true, ["SAM related"] = true, ["Ground Units Non Airdefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Rocket Attack Valid AirDefence"] = true, [27] = true, ["All"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [92] [93] = { ["type"] = "Strela-1 9P31", ["name"] = "SAM SA-9 Strela 1 \"Gaskin\" TEL", ["category"] = "Air Defence", ["description"] = "SAM SA-9 Strela 1 \"Gaskin\" TEL", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, [16] = true, ["SAM related"] = true, ["AA_missile"] = true, ["IR Guided SAM"] = true, ["SAM"] = true, ["NonArmoredUnits"] = true, [25] = true, ["Air Defence"] = true, ["SR SAM"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, [27] = true, ["Armed Air Defence"] = true, ["Ground Units"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "SA-9 Strela-1 9P31", }, -- end of ["aliases"] }, -- end of [93] [94] = { ["type"] = "Strela-10M3", ["name"] = "SAM SA-13 Strela 10M3 \"Gopher\" TEL", ["category"] = "Air Defence", ["description"] = "SAM SA-13 Strela 10M3 \"Gopher\" TEL", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM related"] = true, ["AA_missile"] = true, ["IR Guided SAM"] = true, ["SAM"] = true, ["NonArmoredUnits"] = true, ["Ground vehicles"] = true, ["Air Defence"] = true, [26] = true, ["SR SAM"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, [104] = true, ["Armed Air Defence"] = true, ["Ground Units"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "SA-13 Strela-10M3 9A35M3", }, -- end of ["aliases"] }, -- end of [94] [95] = { ["type"] = "Tor 9A331", ["name"] = "SAM SA-15 Tor \"Gauntlet\"", ["category"] = "Air Defence", ["description"] = "SAM SA-15 Tor \"Gauntlet\"", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["SAM SR"] = true, ["Ground vehicles"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["AA_missile"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["SAM related"] = true, [102] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, [28] = true, ["All"] = true, ["Ground Units"] = true, ["SR SAM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [95] [96] = { ["type"] = "tt_ZU-23", ["name"] = "SPAAA LC with ZU-23", ["category"] = "Air Defence", ["description"] = "SPAAA LC with ZU-23", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["Ground Units"] = true, ["AA_flak"] = true, ["All"] = true, [326] = true, [16] = true, ["AAA"] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["Armed Air Defence"] = true, ["Mobile AAA"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [96] [97] = { ["type"] = "Type_3_80mm_AA", ["name"] = "AAA 80mm Type 3 Flak", ["category"] = "Air Defence", ["description"] = "AAA 80mm Type 3 Flak", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, [2] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, [374] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [97] [98] = { ["type"] = "Type_88_75mm_AA", ["name"] = "AAA 75mm Type 88 Flak", ["category"] = "Air Defence", ["description"] = "AAA 75mm Type 88 Flak", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, [2] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["Armed Air Defence"] = true, [375] = true, ["Air Defence"] = true, ["NonArmoredUnits"] = true, ["Static AAA"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [98] [99] = { ["type"] = "Type_94_25mm_AA_Truck", ["name"] = "AAA 25mm x 2 Type 94 Truck", ["category"] = "Air Defence", ["description"] = "AAA 25mm x 2 Type 94 Truck", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["Ground Units"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Air Defence"] = true, [377] = true, ["Armed Air Defence"] = true, ["Mobile AAA"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [99] [100] = { ["type"] = "Type_96_25mm_AA", ["name"] = "AAA 25mm x 2 Type 96", ["category"] = "Air Defence", ["description"] = "AAA 25mm x 2 Type 96", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["Armed Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, [376] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [100] [101] = { ["type"] = "Ural-375 ZU-23", ["name"] = "AAA ZU-23 on Ural-4320", ["category"] = "Air Defence", ["description"] = "AAA ZU-23 on Ural-4320", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["Ground Units"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, [49] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Air Defence"] = true, ["Armed Air Defence"] = true, ["Mobile AAA"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [101] [102] = { ["type"] = "Ural-375 ZU-23 Insurgent", ["name"] = "AAA ZU-23 on Ural-4320 Insurgent", ["category"] = "Air Defence", ["description"] = "AAA ZU-23 on Ural-4320 Insurgent", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["Ground Units"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [72] = true, ["Air Defence"] = true, ["Armed Air Defence"] = true, ["Mobile AAA"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [102] [103] = { ["type"] = "Vulcan", ["name"] = "SPAAA Vulcan M163", ["category"] = "Air Defence", ["description"] = "SPAAA Vulcan M163", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["SAM elements"] = true, [16] = true, ["Ground vehicles"] = true, [105] = true, ["Mobile AAA"] = true, [46] = true, ["NonArmoredUnits"] = true, ["AAA"] = true, ["Rocket Attack Valid AirDefence"] = true, ["Air Defence"] = true, ["Armed Air Defence"] = true, ["SAM TR"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM related"] = true, ["AA_flak"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "M163 Vulcan", }, -- end of ["aliases"] }, -- end of [103] [104] = { ["type"] = "ZSU-23-4 Shilka", ["name"] = "SPAAA ZSU-23-4 Shilka \"Gun Dish\"", ["category"] = "Air Defence", ["description"] = "SPAAA ZSU-23-4 Shilka \"Gun Dish\"", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Rocket Attack Valid AirDefence"] = true, ["SAM elements"] = true, [16] = true, ["AAA"] = true, ["RADAR_BAND1_FOR_ARM"] = true, [105] = true, ["Mobile AAA"] = true, ["NonArmoredUnits"] = true, ["SAM related"] = true, ["Ground vehicles"] = true, ["Air Defence"] = true, ["Vehicles"] = true, ["AA_flak"] = true, ["NonAndLightArmoredUnits"] = true, ["SAM TR"] = true, ["All"] = true, ["Armed Air Defence"] = true, ["Ground Units"] = true, [30] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [104] [105] = { ["type"] = "ZSU_57_2", ["name"] = "SPAAA ZSU-57-2", ["category"] = "Air Defence", ["description"] = "SPAAA ZSU-57-2", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["Ground Units"] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Air Defence"] = true, [257] = true, ["Armed Air Defence"] = true, ["Mobile AAA"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [105] [106] = { ["type"] = "ZU-23 Closed Insurgent", ["name"] = "AAA ZU-23 Insurgent Closed Emplacement", ["category"] = "Air Defence", ["description"] = "AAA ZU-23 Insurgent Closed Emplacement", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, [2] = true, ["AA_flak"] = true, ["All"] = true, ["Rocket Attack Valid AirDefence"] = true, [16] = true, ["AAA"] = true, ["Armed Air Defence"] = true, ["NonAndLightArmoredUnits"] = true, ["Air Defence"] = true, [71] = true, ["Static AAA"] = true, ["Ground Units"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [106] [107] = { ["type"] = "ZU-23 Emplacement", ["name"] = "AAA ZU-23 Emplacement", ["category"] = "Air Defence", ["description"] = "AAA ZU-23 Emplacement", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, ["NonArmoredUnits"] = true, ["AA_flak"] = true, ["All"] = true, ["AAA"] = true, [16] = true, [47] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [107] [108] = { ["type"] = "ZU-23 Emplacement Closed", ["name"] = "AAA ZU-23 Closed Emplacement", ["category"] = "Air Defence", ["description"] = "AAA ZU-23 Closed Emplacement", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, [48] = true, [26] = true, ["Vehicles"] = true, ["AA_flak"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [16] = true, ["AAA"] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [108] [109] = { ["type"] = "ZU-23 Insurgent", ["name"] = "AAA ZU-23 Insurgent Emplacement", ["category"] = "Air Defence", ["description"] = "AAA ZU-23 Insurgent Emplacement", ["vehicle"] = true, ["attribute"] = { ["Ground vehicles"] = true, ["Vehicles"] = true, [26] = true, [70] = true, ["AA_flak"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [16] = true, ["AAA"] = true, ["Rocket Attack Valid AirDefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Air Defence"] = true, ["Static AAA"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [109] [110] = { ["type"] = "Cow", ["name"] = "Cow", ["category"] = "Animal", ["description"] = "Cow", ["attribute"] = { [5] = true, [9] = true, [100] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [110] [111] = { ["type"] = "AAV7", ["name"] = "APC AAV-7 Amphibious", ["category"] = "Armor", ["description"] = "APC AAV-7 Amphibious", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [111] [112] = { ["type"] = "BMD-1", ["name"] = "IFV BMD-1", ["category"] = "Armor", ["description"] = "IFV BMD-1", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [7] = true, [104] = true, ["Armed vehicles"] = true, ["IFV"] = true, ["All"] = true, ["Ground Units"] = true, ["ATGM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [112] [113] = { ["type"] = "BMP-1", ["name"] = "IFV BMP-1", ["category"] = "Armor", ["description"] = "IFV BMP-1", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [7] = true, [104] = true, ["Armed vehicles"] = true, ["IFV"] = true, ["All"] = true, ["Ground Units"] = true, ["ATGM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [113] [114] = { ["type"] = "BMP-2", ["name"] = "IFV BMP-2", ["category"] = "Armor", ["description"] = "IFV BMP-2", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [7] = true, [104] = true, ["Armed vehicles"] = true, ["IFV"] = true, ["All"] = true, ["Ground Units"] = true, ["ATGM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [114] [115] = { ["type"] = "BMP-3", ["name"] = "IFV BMP-3", ["category"] = "Armor", ["description"] = "IFV BMP-3", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [7] = true, [104] = true, ["Armed vehicles"] = true, ["IFV"] = true, ["All"] = true, ["Ground Units"] = true, ["ATGM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [115] [116] = { ["type"] = "BRDM-2", ["name"] = "Scout BRDM-2", ["category"] = "Armor", ["description"] = "Scout BRDM-2", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [116] [117] = { ["type"] = "BTR-80", ["name"] = "APC BTR-80", ["category"] = "Armor", ["description"] = "APC BTR-80", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [117] [118] = { ["type"] = "BTR-82A", ["name"] = "IFV BTR-82A", ["category"] = "Armor", ["description"] = "IFV BTR-82A", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, [258] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [118] [119] = { ["type"] = "BTR_D", ["name"] = "APC BTR-RD", ["category"] = "Armor", ["description"] = "APC BTR-RD", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, ["ATGM"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [104] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [119] [120] = { ["type"] = "Centaur_IV", ["name"] = "Tk Centaur IV CS", ["category"] = "Armor", ["description"] = "Tk Centaur IV CS", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [120] [121] = { ["type"] = "Challenger2", ["name"] = "MBT Challenger II", ["category"] = "Armor", ["description"] = "MBT Challenger II", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, [2] = true, ["Modern Tanks"] = true, ["Armed vehicles"] = true, [16] = true, [17] = true, ["Ground vehicles"] = true, ["Vehicles"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [121] [122] = { ["type"] = "CHAP_BMPT", ["name"] = "IFV BMPT Terminator [CH]", ["category"] = "Armor", ["description"] = "IFV BMPT Terminator [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["ATGM"] = true, ["Armed vehicles"] = true, [370] = true, ["All"] = true, ["Ground Units"] = true, ["IFV"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [122] [123] = { ["type"] = "CHAP_FV101", ["name"] = "LT FV101 Scorpion [CH]", ["category"] = "Armor", ["description"] = "LT FV101 Scorpion [CH]", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, [351] = true, [26] = true, ["Armed vehicles"] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Vehicles"] = true, [17] = true, ["Tanks"] = true, ["Ground Units Non Airdefence"] = true, ["Ground Units"] = true, ["Armed ground units"] = true, ["All"] = true, ["Datalink"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [123] [124] = { ["type"] = "CHAP_FV107", ["name"] = "Scout FV107 Scimitar [CH]", ["category"] = "Armor", ["description"] = "Scout FV107 Scimitar [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [350] = true, ["Armed vehicles"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [124] [125] = { ["type"] = "CHAP_M1130", ["name"] = "IFV M1130 Stryker CV [CH]", ["category"] = "Armor", ["description"] = "IFV M1130 Stryker CV [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, [359] = true, ["Infantry carriers"] = true, [26] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed vehicles"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [125] [126] = { ["type"] = "CHAP_MATV", ["name"] = "APC MRAP M-ATV [CH]", ["category"] = "Armor", ["description"] = "APC MRAP M-ATV [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed vehicles"] = true, [354] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [126] [127] = { ["type"] = "CHAP_T64BV", ["name"] = "MBT T-64BV Type 2017 [CH]", ["category"] = "Armor", ["description"] = "MBT T-64BV Type 2017 [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["Modern Tanks"] = true, [17] = true, ["Ground vehicles"] = true, [357] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, ["Armed vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Tanks"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [127] [128] = { ["type"] = "CHAP_T84OplotM", ["name"] = "MBT T-84 Oplot-M [CH]", ["category"] = "Armor", ["description"] = "MBT T-84 Oplot-M [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, [356] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Modern Tanks"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, ["Tanks"] = true, ["Armed vehicles"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [128] [129] = { ["type"] = "CHAP_T90M", ["name"] = "MBT T-90M [CH]", ["category"] = "Armor", ["description"] = "MBT T-90M [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["Modern Tanks"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, [352] = true, [26] = true, ["Armed vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Tanks"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [129] [130] = { ["type"] = "Chieftain_mk3", ["name"] = "MBT Chieftain Mk.3", ["category"] = "Armor", ["description"] = "MBT Chieftain Mk.3", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [297] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [130] [131] = { ["type"] = "Churchill_VII", ["name"] = "Tk Churchill VII", ["category"] = "Armor", ["description"] = "Tk Churchill VII", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [131] [132] = { ["type"] = "Cobra", ["name"] = "Scout Cobra", ["category"] = "Armor", ["description"] = "Scout Cobra", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [132] [133] = { ["type"] = "Cromwell_IV", ["name"] = "Tk Cromwell IV", ["category"] = "Armor", ["description"] = "Tk Cromwell IV", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [133] [134] = { ["type"] = "Daimler_AC", ["name"] = "Car Daimler Armored", ["category"] = "Armor", ["description"] = "Car Daimler Armored", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["IFV"] = true, [7] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [134] [135] = { ["type"] = "Elefant_SdKfz_184", ["name"] = "SPG Elefant TD", ["category"] = "Armor", ["description"] = "SPG Elefant TD", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [135] [136] = { ["type"] = "HL_DSHK", ["name"] = "Scout HL with DSHK 12.7mm", ["category"] = "Armor", ["description"] = "Scout HL with DSHK 12.7mm", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, [321] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [136] [137] = { ["type"] = "HL_KORD", ["name"] = "Scout HL with KORD 12.7mm", ["category"] = "Armor", ["description"] = "Scout HL with KORD 12.7mm", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, [322] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [137] [138] = { ["type"] = "Jagdpanther_G1", ["name"] = "SPG Jagdpanther TD", ["category"] = "Armor", ["description"] = "SPG Jagdpanther TD", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [138] [139] = { ["type"] = "JagdPz_IV", ["name"] = "SPG Jagdpanzer IV TD", ["category"] = "Armor", ["description"] = "SPG Jagdpanzer IV TD", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [139] [140] = { ["type"] = "LAV-25", ["name"] = "IFV LAV-25", ["category"] = "Armor", ["description"] = "IFV LAV-25", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, [7] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [140] [141] = { ["type"] = "Leclerc", ["name"] = "MBT Leclerc", ["category"] = "Armor", ["description"] = "MBT Leclerc", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, [2] = true, ["Modern Tanks"] = true, ["Armed vehicles"] = true, [16] = true, [17] = true, ["Ground vehicles"] = true, ["Vehicles"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [141] [142] = { ["type"] = "Leopard-2", ["name"] = "MBT Leopard-2A6M", ["category"] = "Armor", ["description"] = "MBT Leopard-2A6M", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, [2] = true, ["Modern Tanks"] = true, ["Armed vehicles"] = true, ["Ground vehicles"] = true, [17] = true, [299] = true, ["Vehicles"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [142] [143] = { ["type"] = "leopard-2A4", ["name"] = "MBT Leopard-2A4", ["category"] = "Armor", ["description"] = "MBT Leopard-2A4", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, [2] = true, ["Modern Tanks"] = true, ["Armed vehicles"] = true, ["Ground vehicles"] = true, [17] = true, ["Vehicles"] = true, [300] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [143] [144] = { ["type"] = "leopard-2A4_trs", ["name"] = "MBT Leopard-2A4 Trs", ["category"] = "Armor", ["description"] = "MBT Leopard-2A4 Trs", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, [2] = true, ["Modern Tanks"] = true, ["Armed vehicles"] = true, ["Ground vehicles"] = true, [17] = true, ["Vehicles"] = true, ["Ground Units Non Airdefence"] = true, [301] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [144] [145] = { ["type"] = "Leopard-2A5", ["name"] = "MBT Leopard-2A5", ["category"] = "Armor", ["description"] = "MBT Leopard-2A5", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, [2] = true, ["Modern Tanks"] = true, ["Armed vehicles"] = true, ["Ground vehicles"] = true, [298] = true, ["Vehicles"] = true, [17] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [145] [146] = { ["type"] = "Leopard1A3", ["name"] = "MBT Leopard 1A3", ["category"] = "Armor", ["description"] = "MBT Leopard 1A3", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "LEO1A3", }, -- end of ["aliases"] }, -- end of [146] [147] = { ["type"] = "M-1 Abrams", ["name"] = "MBT M1A2 Abrams", ["category"] = "Armor", ["description"] = "MBT M1A2 Abrams", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["Modern Tanks"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, [16] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, ["Tanks"] = true, ["Armed vehicles"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [147] [148] = { ["type"] = "M-113", ["name"] = "APC M113", ["category"] = "Armor", ["description"] = "APC M113", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["Armed vehicles"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [148] [149] = { ["type"] = "M-2 Bradley", ["name"] = "IFV M2A2 Bradley", ["category"] = "Armor", ["description"] = "IFV M2A2 Bradley", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["IFV"] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed vehicles"] = true, [7] = true, [104] = true, ["Infantry carriers"] = true, ["Ground Units"] = true, ["All"] = true, ["Datalink"] = true, ["ATGM"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "M2A2 Bradley", }, -- end of ["aliases"] }, -- end of [149] [150] = { ["type"] = "M-60", ["name"] = "MBT M60A3 Patton", ["category"] = "Armor", ["description"] = "MBT M60A3 Patton", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [150] [151] = { ["type"] = "M1043 HMMWV Armament", ["name"] = "Scout HMMWV", ["category"] = "Armor", ["description"] = "Scout HMMWV", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [14] = true, ["Armed vehicles"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [151] [152] = { ["type"] = "M1045 HMMWV TOW", ["name"] = "ATGM HMMWV", ["category"] = "Armor", ["description"] = "ATGM HMMWV", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed vehicles"] = true, ["ATGM"] = true, [14] = true, [104] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [152] [153] = { ["type"] = "M10_GMC", ["name"] = "SPG M10 GMC TD", ["category"] = "Armor", ["description"] = "SPG M10 GMC TD", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [153] [154] = { ["type"] = "M1126 Stryker ICV", ["name"] = "IFV M1126 Stryker ICV", ["category"] = "Armor", ["description"] = "IFV M1126 Stryker ICV", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, [80] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["Armed vehicles"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [154] [155] = { ["type"] = "M1128 Stryker MGS", ["name"] = "SPG Stryker MGS", ["category"] = "Armor", ["description"] = "SPG Stryker MGS", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["Modern Tanks"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, [80] = true, ["Ground Units Non Airdefence"] = true, ["IFV"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, ["Armed vehicles"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Tanks"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [155] [156] = { ["type"] = "M1134 Stryker ATGM", ["name"] = "ATGM Stryker", ["category"] = "Armor", ["description"] = "ATGM Stryker", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, [80] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed vehicles"] = true, [104] = true, ["IFV"] = true, ["Ground Units"] = true, ["All"] = true, ["Datalink"] = true, ["ATGM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [156] [157] = { ["type"] = "M1A2C_SEP_V3", ["name"] = "MBT M1A2C SEP v3 Abrams", ["category"] = "Armor", ["description"] = "MBT M1A2C SEP v3 Abrams", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["Modern Tanks"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, [16] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, ["Tanks"] = true, ["Armed vehicles"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [157] [158] = { ["type"] = "M2A1_halftrack", ["name"] = "APC M2A1 Halftrack", ["category"] = "Armor", ["description"] = "APC M2A1 Halftrack", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [158] [159] = { ["type"] = "M4_Sherman", ["name"] = "Tk M4 Sherman", ["category"] = "Armor", ["description"] = "Tk M4 Sherman", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [159] [160] = { ["type"] = "M4_Tractor", ["name"] = "Tractor M4 High Speed", ["category"] = "Armor", ["description"] = "Tractor M4 High Speed", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["IFV"] = true, [7] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [160] [161] = { ["type"] = "M4A4_Sherman_FF", ["name"] = "Tk M4A4 Sherman Firefly", ["category"] = "Armor", ["description"] = "Tk M4A4 Sherman Firefly", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [161] [162] = { ["type"] = "M8_Greyhound", ["name"] = "Scout M8 Greyhound AC", ["category"] = "Armor", ["description"] = "Scout M8 Greyhound AC", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["IFV"] = true, [7] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [162] [163] = { ["type"] = "Marder", ["name"] = "IFV Marder", ["category"] = "Armor", ["description"] = "IFV Marder", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["ATGM"] = true, [7] = true, ["Armed vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["IFV"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [163] [164] = { ["type"] = "MaxxPro_MRAP", ["name"] = "APC MRAP MaxxPro", ["category"] = "Armor", ["description"] = "APC MRAP MaxxPro", ["vehicle"] = true, ["attribute"] = { [347] = true, [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["Armed vehicles"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [164] [165] = { ["type"] = "MCV-80", ["name"] = "IFV Warrior ", ["category"] = "Armor", ["description"] = "IFV Warrior ", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [165] [166] = { ["type"] = "Merkava_Mk4", ["name"] = "MBT Merkava IV", ["category"] = "Armor", ["description"] = "MBT Merkava IV", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["Modern Tanks"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, [16] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, ["Tanks"] = true, ["Armed vehicles"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [166] [167] = { ["type"] = "MTLB", ["name"] = "APC MTLB", ["category"] = "Armor", ["description"] = "APC MTLB", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [167] [168] = { ["type"] = "PT_76", ["name"] = "LT PT-76", ["category"] = "Armor", ["description"] = "LT PT-76", ["vehicle"] = true, ["attribute"] = { ["Tanks"] = true, [26] = true, ["HeavyArmoredUnits"] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [296] = true, ["Armed vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Vehicles"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [168] [169] = { ["type"] = "Pz_IV_H", ["name"] = "Tk PzIV H", ["category"] = "Armor", ["description"] = "Tk PzIV H", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [169] [170] = { ["type"] = "Pz_V_Panther_G", ["name"] = "Tk Panther G (Pz V)", ["category"] = "Armor", ["description"] = "Tk Panther G (Pz V)", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [170] [171] = { ["type"] = "Sd_Kfz_234_2_Puma", ["name"] = "Scout Puma AC", ["category"] = "Armor", ["description"] = "Scout Puma AC", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["IFV"] = true, [7] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [171] [172] = { ["type"] = "Sd_Kfz_251", ["name"] = "APC Sd.Kfz.251 Halftrack", ["category"] = "Armor", ["description"] = "APC Sd.Kfz.251 Halftrack", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [172] [173] = { ["type"] = "Stug_III", ["name"] = "SPG StuG III G AG", ["category"] = "Armor", ["description"] = "SPG StuG III G AG", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [173] [174] = { ["type"] = "Stug_IV", ["name"] = "SPG StuG IV AG", ["category"] = "Armor", ["description"] = "SPG StuG IV AG", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [174] [175] = { ["type"] = "SturmPzIV", ["name"] = "SPG Brummbaer AG", ["category"] = "Armor", ["description"] = "SPG Brummbaer AG", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["LightArmoredUnits"] = true, ["Armed vehicles"] = true, [16] = true, [17] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [175] [176] = { ["type"] = "T-55", ["name"] = "MBT T-55", ["category"] = "Armor", ["description"] = "MBT T-55", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [176] [177] = { ["type"] = "T-72B", ["name"] = "MBT T-72B", ["category"] = "Armor", ["description"] = "MBT T-72B", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, [2] = true, ["Modern Tanks"] = true, ["Armed vehicles"] = true, [16] = true, [17] = true, ["Ground vehicles"] = true, ["Vehicles"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [177] [178] = { ["type"] = "T-72B3", ["name"] = "MBT T-72B3", ["category"] = "Armor", ["description"] = "MBT T-72B3", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, [2] = true, ["Modern Tanks"] = true, ["Armed vehicles"] = true, [16] = true, [17] = true, ["Ground vehicles"] = true, ["Vehicles"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [178] [179] = { ["type"] = "T-80UD", ["name"] = "MBT T-80U", ["category"] = "Armor", ["description"] = "MBT T-80U", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["AntiAir Armed Vehicles"] = true, [2] = true, ["Modern Tanks"] = true, ["Armed vehicles"] = true, [16] = true, [17] = true, ["Ground vehicles"] = true, ["Vehicles"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [179] [180] = { ["type"] = "T-90", ["name"] = "MBT T-90A [CH]", ["category"] = "Armor", ["description"] = "MBT T-90A [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["Modern Tanks"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, [358] = true, ["Ground Units Non Airdefence"] = true, [26] = true, ["Armed vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Tanks"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [180] [181] = { ["type"] = "Tetrarch", ["name"] = "Tk Tetrach", ["category"] = "Armor", ["description"] = "Tk Tetrach", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["IFV"] = true, [7] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [181] [182] = { ["type"] = "Tiger_I", ["name"] = "Tk Tiger 1", ["category"] = "Armor", ["description"] = "Tk Tiger 1", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [182] [183] = { ["type"] = "Tiger_II_H", ["name"] = "Tk Tiger II", ["category"] = "Armor", ["description"] = "Tk Tiger II", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [183] [184] = { ["type"] = "TPZ", ["name"] = "APC TPz Fuchs ", ["category"] = "Armor", ["description"] = "APC TPz Fuchs ", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [184] [185] = { ["type"] = "tt_DSHK", ["name"] = "Scout LC with DSHK 12.7mm", ["category"] = "Armor", ["description"] = "Scout LC with DSHK 12.7mm", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, [323] = true, ["All"] = true, ["Ground Units"] = true, ["Armored vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [185] [186] = { ["type"] = "tt_KORD", ["name"] = "Scout LC with KORD 12.7mm", ["category"] = "Armor", ["description"] = "Scout LC with KORD 12.7mm", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["Armed vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [324] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [186] [187] = { ["type"] = "TYPE-59", ["name"] = "MT Type 59", ["category"] = "Armor", ["description"] = "MT Type 59", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [187] [188] = { ["type"] = "Type_89_I_Go", ["name"] = "Tk Type 89 I Go", ["category"] = "Armor", ["description"] = "Tk Type 89 I Go", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [188] [189] = { ["type"] = "Type_98_Ke_Ni", ["name"] = "Tk Type 98 Ke Ni", ["category"] = "Armor", ["description"] = "Tk Type 98 Ke Ni", ["vehicle"] = true, ["attribute"] = { ["HeavyArmoredUnits"] = true, ["Tanks"] = true, [26] = true, ["Armed vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Old Tanks"] = true, [17] = true, ["Vehicles"] = true, [2] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [189] [190] = { ["type"] = "VAB_Mephisto", ["name"] = "ATGM VAB Mephisto", ["category"] = "Armor", ["description"] = "ATGM VAB Mephisto", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, [80] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed vehicles"] = true, [104] = true, ["IFV"] = true, ["Ground Units"] = true, ["All"] = true, ["Datalink"] = true, ["ATGM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [190] [191] = { ["type"] = "ZBD04A", ["name"] = "ZBD-04A", ["category"] = "Armor", ["description"] = "ZBD-04A", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, ["IFV"] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed vehicles"] = true, [276] = true, [104] = true, ["Infantry carriers"] = true, ["Ground Units"] = true, ["All"] = true, ["Datalink"] = true, ["ATGM"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [191] [192] = { ["type"] = "ZTZ96B", ["name"] = "ZTZ-96B", ["category"] = "Armor", ["description"] = "ZTZ-96B", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["Modern Tanks"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Ground Units Non Airdefence"] = true, [275] = true, [26] = true, ["Armed vehicles"] = true, ["Tanks"] = true, ["AntiAir Armed Vehicles"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [192] [193] = { ["type"] = "2B11 mortar", ["name"] = "Mortar 2B11 120mm", ["category"] = "Artillery", ["description"] = "Mortar 2B11 120mm", ["vehicle"] = true, ["attribute"] = { [1] = true, [2] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [193] [194] = { ["type"] = "CHAP_M142_ATACMS_M39A1", ["name"] = "MLRS M142 HIMARS ATACMS CM [CH]", ["category"] = "Artillery", ["description"] = "MLRS M142 HIMARS ATACMS CM [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, [366] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, [27] = true, ["Indirect fire"] = true, ["All"] = true, ["Datalink"] = true, ["MLRS"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [194] [195] = { ["type"] = "CHAP_M142_ATACMS_M48", ["name"] = "MLRS M142 HIMARS ATACMS HE [CH]", ["category"] = "Artillery", ["description"] = "MLRS M142 HIMARS ATACMS HE [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [367] = true, [27] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units"] = true, ["Indirect fire"] = true, ["All"] = true, ["Datalink"] = true, ["MLRS"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [195] [196] = { ["type"] = "CHAP_M142_GMLRS_M30", ["name"] = "MLRS M142 HIMARS GMLRS CM [CH]", ["category"] = "Artillery", ["description"] = "MLRS M142 HIMARS GMLRS CM [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [364] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, [27] = true, ["NonAndLightArmoredUnits"] = true, [17] = true, ["Indirect fire"] = true, ["MLRS"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [196] [197] = { ["type"] = "CHAP_M142_GMLRS_M31", ["name"] = "MLRS M142 HIMARS GMLRS HE [CH]", ["category"] = "Artillery", ["description"] = "MLRS M142 HIMARS GMLRS HE [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Ground vehicles"] = true, [365] = true, ["Armed ground units"] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [27] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units"] = true, ["Indirect fire"] = true, ["All"] = true, ["Datalink"] = true, ["MLRS"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [197] [198] = { ["type"] = "CHAP_TOS1A", ["name"] = "MLRS TOS-1A Solntsepyok [CH]", ["category"] = "Artillery", ["description"] = "MLRS TOS-1A Solntsepyok [CH]", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [2] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Armed ground units"] = true, ["MLRS"] = true, ["Ground Units"] = true, [""] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [198] [199] = { ["type"] = "Grad-URAL", ["name"] = "MLRS BM-21 Grad 122mm", ["category"] = "Artillery", ["description"] = "MLRS BM-21 Grad 122mm", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, [63] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["MLRS"] = true, ["Ground Units"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "MLRS BM-21 Grad", }, -- end of ["aliases"] }, -- end of [199] [200] = { ["type"] = "Grad_FDDM", ["name"] = "Grad MRL FDDM (FC)", ["category"] = "Artillery", ["description"] = "Grad MRL FDDM (FC)", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "Boman", }, -- end of ["aliases"] }, -- end of [200] [201] = { ["type"] = "HL_B8M1", ["name"] = "MLRS HL with B8M1 80mm", ["category"] = "Artillery", ["description"] = "MLRS HL with B8M1 80mm", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, [""] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["NonAndLightArmoredUnits"] = true, [63] = true, ["Indirect fire"] = true, ["MLRS"] = true, ["Ground Units"] = true, [27] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [201] [202] = { ["type"] = "L118_Unit", ["name"] = "L118 Light Artillery Gun", ["category"] = "Artillery", ["description"] = "L118 Light Artillery Gun", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["LightArmoredUnits"] = true, [8] = true, ["Armed vehicles"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [349] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [202] [203] = { ["type"] = "LeFH_18-40-105", ["name"] = "FH LeFH-18 105mm", ["category"] = "Artillery", ["description"] = "FH LeFH-18 105mm", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [319] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [203] [204] = { ["type"] = "M-109", ["name"] = "SPH M109 Paladin 155mm", ["category"] = "Artillery", ["description"] = "SPH M109 Paladin 155mm", ["vehicle"] = true, ["attribute"] = { [1] = true, [2] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, [17] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "M109", }, -- end of ["aliases"] }, -- end of [204] [205] = { ["type"] = "M12_GMC", ["name"] = "SPH M12 GMC 155mm", ["category"] = "Artillery", ["description"] = "SPH M12 GMC 155mm", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["LightArmoredUnits"] = true, ["Armed vehicles"] = true, [16] = true, [17] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [205] [206] = { ["type"] = "M2A1-105", ["name"] = "FH M2A1 105mm", ["category"] = "Artillery", ["description"] = "FH M2A1 105mm", ["vehicle"] = true, ["attribute"] = { [320] = true, ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Artillery"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [206] [207] = { ["type"] = "MLRS", ["name"] = "MLRS M270 227mm", ["category"] = "Artillery", ["description"] = "MLRS M270 227mm", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, ["Ground Units"] = true, [63] = true, ["NonAndLightArmoredUnits"] = true, [27] = true, ["Indirect fire"] = true, ["All"] = true, ["Datalink"] = true, ["MLRS"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "M270 MLRS", }, -- end of ["aliases"] }, -- end of [207] [208] = { ["type"] = "MLRS FDDM", ["name"] = "MRLS FDDM (FC)", ["category"] = "Artillery", ["description"] = "MRLS FDDM (FC)", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [14] = true, ["Armed vehicles"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [208] [209] = { ["type"] = "Pak40", ["name"] = "FH Pak 40 75mm", ["category"] = "Artillery", ["description"] = "FH Pak 40 75mm", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [318] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [209] [210] = { ["type"] = "PLZ05", ["name"] = "PLZ-05", ["category"] = "Artillery", ["description"] = "PLZ-05", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [279] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [210] [211] = { ["type"] = "SAU 2-C9", ["name"] = "SPM 2S9 Nona 120mm M", ["category"] = "Artillery", ["description"] = "SPM 2S9 Nona 120mm M", ["vehicle"] = true, ["attribute"] = { [1] = true, [2] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [211] [212] = { ["type"] = "SAU Akatsia", ["name"] = "SPH 2S3 Akatsia 152mm", ["category"] = "Artillery", ["description"] = "SPH 2S3 Akatsia 152mm", ["vehicle"] = true, ["attribute"] = { [1] = true, [2] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "2S3 Akatsia", }, -- end of ["aliases"] }, -- end of [212] [213] = { ["type"] = "SAU Gvozdika", ["name"] = "SPH 2S1 Gvozdika 122mm", ["category"] = "Artillery", ["description"] = "SPH 2S1 Gvozdika 122mm", ["vehicle"] = true, ["attribute"] = { [1] = true, [2] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [213] [214] = { ["type"] = "SAU Msta", ["name"] = "SPH 2S19 Msta 152mm", ["category"] = "Artillery", ["description"] = "SPH 2S19 Msta 152mm", ["vehicle"] = true, ["attribute"] = { [1] = true, [2] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "2S19 Msta", }, -- end of ["aliases"] }, -- end of [214] [215] = { ["type"] = "Smerch", ["name"] = "MLRS 9A52 Smerch CM 300mm", ["category"] = "Artillery", ["description"] = "MLRS 9A52 Smerch CM 300mm", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, [63] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["MLRS"] = true, ["Ground Units"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [215] [216] = { ["type"] = "Smerch_HE", ["name"] = "MLRS 9A52 Smerch HE 300mm", ["category"] = "Artillery", ["description"] = "MLRS 9A52 Smerch HE 300mm", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, [63] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["MLRS"] = true, ["Ground Units"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [216] [217] = { ["type"] = "SpGH_Dana", ["name"] = "SPH Dana vz77 152mm", ["category"] = "Artillery", ["description"] = "SPH Dana vz77 152mm", ["vehicle"] = true, ["attribute"] = { [1] = true, [2] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [217] [218] = { ["type"] = "T155_Firtina", ["name"] = "SPH T155 Firtina 155mm", ["category"] = "Artillery", ["description"] = "SPH T155 Firtina 155mm", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, ["Vehicles"] = true, [2] = true, ["LightArmoredUnits"] = true, ["Indirect fire"] = true, ["Armed vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Armed ground units"] = true, [302] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [218] [219] = { ["type"] = "tt_B8M1", ["name"] = "MLRS LC with B8M1 80mm", ["category"] = "Artillery", ["description"] = "MLRS LC with B8M1 80mm", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, [""] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["NonAndLightArmoredUnits"] = true, [63] = true, ["Indirect fire"] = true, ["MLRS"] = true, ["Ground Units"] = true, [27] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [219] [220] = { ["type"] = "Uragan_BM-27", ["name"] = "MLRS 9K57 Uragan BM-27 220mm", ["category"] = "Artillery", ["description"] = "MLRS 9K57 Uragan BM-27 220mm", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, [63] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["MLRS"] = true, ["Ground Units"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [220] [221] = { ["type"] = "Wespe124", ["name"] = "SPH Sd.Kfz.124 Wespe 105mm", ["category"] = "Artillery", ["description"] = "SPH Sd.Kfz.124 Wespe 105mm", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["LightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Vehicles"] = true, [317] = true, ["All"] = true, ["Ground Units"] = true, ["Armed ground units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [221] [222] = { ["desc"] = { ["maxMass"] = 2000, ["minMass"] = 1000, }, -- end of ["desc"] ["type"] = "ammo_cargo", ["name"] = "Ammo", ["category"] = "Cargo", ["defaultMass"] = 1500, ["description"] = "Ammo", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [222] [223] = { ["desc"] = { ["maxMass"] = 480, ["minMass"] = 100, }, -- end of ["desc"] ["type"] = "barrels_cargo", ["name"] = "Barrels", ["category"] = "Cargo", ["defaultMass"] = 480, ["description"] = "Barrels", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [223] [224] = { ["desc"] = { ["maxMass"] = 4000, ["minMass"] = 100, }, -- end of ["desc"] ["type"] = "container_cargo", ["name"] = "Container", ["category"] = "Cargo", ["defaultMass"] = 1200, ["description"] = "Container", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [224] [225] = { ["desc"] = { ["maxMass"] = 823, ["minMass"] = 823, }, -- end of ["desc"] ["type"] = "f_bar_cargo", ["name"] = "F-shape barrier", ["category"] = "Cargo", ["defaultMass"] = 823, ["description"] = "F-shape barrier", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [225] [226] = { ["desc"] = { ["maxMass"] = 5000, ["minMass"] = 800, }, -- end of ["desc"] ["type"] = "fueltank_cargo", ["name"] = "Fueltank", ["category"] = "Cargo", ["defaultMass"] = 2400, ["description"] = "Fueltank", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [226] [227] = { ["desc"] = { ["maxMass"] = 10000, ["minMass"] = 10000, }, -- end of ["desc"] ["type"] = "gbu_43b_airdrop", ["name"] = "GBU-43 MOAB", ["category"] = "Cargo", ["defaultMass"] = 10000, ["description"] = "GBU-43 MOAB", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [227] [228] = { ["desc"] = { ["maxMass"] = 10000, ["minMass"] = 3800, }, -- end of ["desc"] ["type"] = "iso_container", ["name"] = "ISO container", ["category"] = "Cargo", ["defaultMass"] = 4500, ["description"] = "ISO container", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [228] [229] = { ["desc"] = { ["maxMass"] = 10000, ["minMass"] = 2200, }, -- end of ["desc"] ["type"] = "iso_container_small", ["name"] = "ISO container small", ["category"] = "Cargo", ["defaultMass"] = 3200, ["description"] = "ISO container small", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [229] [230] = { ["desc"] = { ["maxMass"] = 8100, ["minMass"] = 100, }, -- end of ["desc"] ["type"] = "l118", ["name"] = "L118 Light Artillery", ["category"] = "Cargo", ["defaultMass"] = 2100, ["description"] = "L118 Light Artillery", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [230] [231] = { ["desc"] = { ["maxMass"] = 840, ["minMass"] = 840, }, -- end of ["desc"] ["type"] = "m117_cargo", ["name"] = "M117 bombs", ["category"] = "Cargo", ["defaultMass"] = 840, ["description"] = "M117 bombs", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [231] [232] = { ["desc"] = { ["maxMass"] = 5000, ["minMass"] = 700, }, -- end of ["desc"] ["type"] = "oiltank_cargo", ["name"] = "Oiltank", ["category"] = "Cargo", ["defaultMass"] = 2300, ["description"] = "Oiltank", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [232] [233] = { ["desc"] = { ["maxMass"] = 4815, ["minMass"] = 4815, }, -- end of ["desc"] ["type"] = "pipes_big_cargo", ["name"] = "Pipes big", ["category"] = "Cargo", ["defaultMass"] = 4815, ["description"] = "Pipes big", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [233] [234] = { ["desc"] = { ["maxMass"] = 4350, ["minMass"] = 4350, }, -- end of ["desc"] ["type"] = "pipes_small_cargo", ["name"] = "Pipes small", ["category"] = "Cargo", ["defaultMass"] = 4350, ["description"] = "Pipes small", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [234] [235] = { ["desc"] = { ["maxMass"] = 5000, ["minMass"] = 5000, }, -- end of ["desc"] ["type"] = "tetrapod_cargo", ["name"] = "Tetrapod", ["category"] = "Cargo", ["defaultMass"] = 5000, ["description"] = "Tetrapod", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [235] [236] = { ["desc"] = { ["maxMass"] = 4747, ["minMass"] = 4747, }, -- end of ["desc"] ["type"] = "trunks_long_cargo", ["name"] = "Trunks long", ["category"] = "Cargo", ["defaultMass"] = 4747, ["description"] = "Trunks long", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [236] [237] = { ["desc"] = { ["maxMass"] = 5000, ["minMass"] = 5000, }, -- end of ["desc"] ["type"] = "trunks_small_cargo", ["name"] = "Trunks short", ["category"] = "Cargo", ["defaultMass"] = 5000, ["description"] = "Trunks short", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [237] [238] = { ["desc"] = { ["maxMass"] = 10000, ["minMass"] = 100, }, -- end of ["desc"] ["type"] = "uh1h_cargo", ["name"] = "UH-1H cargo", ["category"] = "Cargo", ["defaultMass"] = 1000, ["description"] = "UH-1H cargo", ["attribute"] = { ["Cargos"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [238] [239] = { ["type"] = "Boxcartrinity", ["name"] = "Flatcar", ["category"] = "Carriage", ["description"] = "Flatcar", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [51] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [239] [240] = { ["type"] = "Coach a passenger", ["name"] = "Passenger Car", ["category"] = "Carriage", ["description"] = "Passenger Car", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [54] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [240] [241] = { ["type"] = "Coach a platform", ["name"] = "Coach Platform", ["category"] = "Carriage", ["description"] = "Coach Platform", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, [53] = true, ["Unarmed vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [241] [242] = { ["type"] = "Coach a tank blue", ["name"] = "Tank Car blue", ["category"] = "Carriage", ["description"] = "Tank Car blue", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, [50] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [242] [243] = { ["type"] = "Coach a tank yellow", ["name"] = "Tank Car yellow", ["category"] = "Carriage", ["description"] = "Tank Car yellow", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [98] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [243] [244] = { ["type"] = "Coach cargo", ["name"] = "Freight Van", ["category"] = "Carriage", ["description"] = "Freight Van", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [51] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [244] [245] = { ["type"] = "Coach cargo open", ["name"] = "Open Wagon", ["category"] = "Carriage", ["description"] = "Open Wagon", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [51] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [245] [246] = { ["type"] = "DR_50Ton_Flat_Wagon", ["name"] = "DR 50-ton flat wagon", ["category"] = "Carriage", ["description"] = "DR 50-ton flat wagon", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, [53] = true, ["Unarmed vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [246] [247] = { ["type"] = "German_covered_wagon_G10", ["name"] = "Wagon G10 (Germany)", ["category"] = "Carriage", ["description"] = "Wagon G10 (Germany)", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [51] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [247] [248] = { ["type"] = "German_tank_wagon", ["name"] = "Tank Car (Germany)", ["category"] = "Carriage", ["description"] = "Tank Car (Germany)", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Unarmed vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [248] [249] = { ["type"] = "Tankcartrinity", ["name"] = "Tank Cartrinity", ["category"] = "Carriage", ["description"] = "Tank Cartrinity", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [51] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [249] [250] = { ["type"] = "Wellcarnsc", ["name"] = "Well Car", ["category"] = "Carriage", ["description"] = "Well Car", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, [51] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [250] [251] = { ["type"] = "big_smoke", ["name"] = "Big smoke", ["category"] = "Effect", ["description"] = "Big smoke", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [251] [252] = { ["type"] = "wp_marker", ["name"] = "WP Marker", ["category"] = "Effect", ["description"] = "WP Marker", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [252] [253] = { ["type"] = ".Command Center", ["name"] = "Command Center", ["category"] = "Fortification", ["description"] = "Command Center", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [253] [254] = { ["type"] = "345 Excavator", ["name"] = "Excavator", ["category"] = "Fortification", ["description"] = "Excavator", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [254] [255] = { ["type"] = "af_hq", ["name"] = "HQ Building", ["category"] = "Fortification", ["description"] = "HQ Building", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [255] [256] = { ["type"] = "Airshow_Cone", ["name"] = "Airshow cone", ["category"] = "Fortification", ["description"] = "Airshow cone", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [256] [257] = { ["type"] = "Airshow_Crowd", ["name"] = "Airshow Crowd", ["category"] = "Fortification", ["description"] = "Airshow Crowd", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [257] [258] = { ["type"] = "AM32a-60_01", ["name"] = "M92 AM32a-60-01", ["category"] = "Fortification", ["description"] = "M92 AM32a-60-01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [258] [259] = { ["type"] = "AM32a-60_02", ["name"] = "M92 AM32a-60-02", ["category"] = "Fortification", ["description"] = "M92 AM32a-60-02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [259] [260] = { ["type"] = "APFC fuel", ["name"] = "M92 APFC fuel", ["category"] = "Fortification", ["description"] = "M92 APFC fuel", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [260] [261] = { ["type"] = "B600", ["name"] = "M92 B600", ["category"] = "Fortification", ["description"] = "M92 B600", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [261] [262] = { ["type"] = "Barracks 2", ["name"] = "Barracks 2", ["category"] = "Fortification", ["description"] = "Barracks 2", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [262] [263] = { ["type"] = "Barrier A", ["name"] = "M92 Barrier A", ["category"] = "Fortification", ["description"] = "M92 Barrier A", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [263] [264] = { ["type"] = "Barrier B", ["name"] = "M92 Barrier B", ["category"] = "Fortification", ["description"] = "M92 Barrier B", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [264] [265] = { ["type"] = "Barrier C", ["name"] = "M92 Barrier C", ["category"] = "Fortification", ["description"] = "M92 Barrier C", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [265] [266] = { ["type"] = "Barrier D", ["name"] = "M92 Barrier D", ["category"] = "Fortification", ["description"] = "M92 Barrier D", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [266] [267] = { ["type"] = "Beer Bomb", ["name"] = "Barrel", ["category"] = "Fortification", ["description"] = "Barrel", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [267] [268] = { ["type"] = "Belgian gate", ["name"] = "Belgian gate", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "Belgian gate", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [268] [269] = { ["type"] = "billboard_motorized", ["name"] = "Billboard Motorized", ["category"] = "Fortification", ["description"] = "Billboard Motorized", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [269] [270] = { ["type"] = "Black_Tyre", ["name"] = "Mark Tyre Black", ["category"] = "Fortification", ["description"] = "Mark Tyre Black", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [270] [271] = { ["type"] = "Black_Tyre_RF", ["name"] = "Mark Tyre with Red Flag", ["category"] = "Fortification", ["description"] = "Mark Tyre with Red Flag", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [271] [272] = { ["type"] = "Black_Tyre_WF", ["name"] = "Mark Tyre with White Flag", ["category"] = "Fortification", ["description"] = "Mark Tyre with White Flag", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [272] [273] = { ["type"] = "Boiler-house A", ["name"] = "Boiler-house A", ["category"] = "Fortification", ["description"] = "Boiler-house A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [273] [274] = { ["type"] = "BoomBarrier_closed", ["name"] = "M92 Boom Barrier closed", ["category"] = "Fortification", ["description"] = "M92 Boom Barrier closed", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [274] [275] = { ["type"] = "BoomBarrier_open", ["name"] = "M92 Boom Barrier open", ["category"] = "Fortification", ["description"] = "M92 Boom Barrier open", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [275] [276] = { ["type"] = "Building01_PBR", ["name"] = "M92 Building01 PBR", ["category"] = "Fortification", ["description"] = "M92 Building01 PBR", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [276] [277] = { ["type"] = "Building02_PBR", ["name"] = "M92 Building02 PBR", ["category"] = "Fortification", ["description"] = "M92 Building02 PBR", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [277] [278] = { ["type"] = "Building03_PBR", ["name"] = "M92 Building03 PBR", ["category"] = "Fortification", ["description"] = "M92 Building03 PBR", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [278] [279] = { ["type"] = "Building04_PBR", ["name"] = "M92 Building04 PBR", ["category"] = "Fortification", ["description"] = "M92 Building04 PBR", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [279] [280] = { ["type"] = "Building05_PBR", ["name"] = "M92 Building05 PBR", ["category"] = "Fortification", ["description"] = "M92 Building05 PBR", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [280] [281] = { ["type"] = "Building06_PBR", ["name"] = "M92 Building06 PBR", ["category"] = "Fortification", ["description"] = "M92 Building06 PBR", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [281] [282] = { ["type"] = "Building07_PBR", ["name"] = "M92 Building07 PBR", ["category"] = "Fortification", ["description"] = "M92 Building07 PBR", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [282] [283] = { ["type"] = "Building08_PBR", ["name"] = "M92 Building08 PBR", ["category"] = "Fortification", ["description"] = "M92 Building08 PBR", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [283] [284] = { ["type"] = "Bunker", ["name"] = "Bunker 2", ["category"] = "Fortification", ["description"] = "Bunker 2", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [284] [285] = { ["type"] = "Cafe", ["name"] = "Cafe", ["category"] = "Fortification", ["description"] = "Cafe", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [285] [286] = { ["type"] = "Camouflage01", ["name"] = "M92 Camouflage 01", ["category"] = "Fortification", ["description"] = "M92 Camouflage 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [286] [287] = { ["type"] = "Camouflage02", ["name"] = "M92 Camouflage 02", ["category"] = "Fortification", ["description"] = "M92 Camouflage 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [287] [288] = { ["type"] = "Camouflage03", ["name"] = "M92 Camouflage 03", ["category"] = "Fortification", ["description"] = "M92 Camouflage 03", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [288] [289] = { ["type"] = "Camouflage04", ["name"] = "M92 Camouflage 04", ["category"] = "Fortification", ["description"] = "M92 Camouflage 04", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [289] [290] = { ["type"] = "Camouflage05", ["name"] = "M92 Camouflage 05", ["category"] = "Fortification", ["description"] = "M92 Camouflage 05", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [290] [291] = { ["type"] = "Camouflage06", ["name"] = "M92 Camouflage 06", ["category"] = "Fortification", ["description"] = "M92 Camouflage 06", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [291] [292] = { ["type"] = "Camouflage07", ["name"] = "M92 Camouflage 07", ["category"] = "Fortification", ["description"] = "M92 Camouflage 07", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [292] [293] = { ["type"] = "Cargo01", ["name"] = "M92 Cargo 01", ["category"] = "Fortification", ["description"] = "M92 Cargo 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [293] [294] = { ["type"] = "Cargo02", ["name"] = "M92 Cargo 02", ["category"] = "Fortification", ["description"] = "M92 Cargo 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [294] [295] = { ["type"] = "Cargo03", ["name"] = "M92 Cargo 03", ["category"] = "Fortification", ["description"] = "M92 Cargo 03", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [295] [296] = { ["type"] = "Cargo04", ["name"] = "M92 Cargo 04", ["category"] = "Fortification", ["description"] = "M92 Cargo 04", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [296] [297] = { ["type"] = "Cargo05", ["name"] = "M92 Cargo 05", ["category"] = "Fortification", ["description"] = "M92 Cargo 05", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [297] [298] = { ["type"] = "Cargo06", ["name"] = "M92 Cargo 06", ["category"] = "Fortification", ["description"] = "M92 Cargo 06", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [298] [299] = { ["type"] = "Chemical tank A", ["name"] = "Chemical tank A", ["category"] = "Fortification", ["description"] = "Chemical tank A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [299] [300] = { ["type"] = "Comms tower M", ["name"] = "Comms tower M", ["category"] = "Fortification", ["description"] = "Comms tower M", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [300] [301] = { ["type"] = "Concertina wire", ["name"] = "Concertina wire", ["category"] = "Fortification", ["description"] = "Concertina wire", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [301] [302] = { ["type"] = "Cone01", ["name"] = "M92 Cone 01", ["category"] = "Fortification", ["description"] = "M92 Cone 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [302] [303] = { ["type"] = "Cone02", ["name"] = "M92 Cone 02", ["category"] = "Fortification", ["description"] = "M92 Cone 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [303] [304] = { ["type"] = "Container brown", ["name"] = "Container brown", ["category"] = "Fortification", ["description"] = "Container brown", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [304] [305] = { ["type"] = "Container red 1", ["name"] = "Container red 1", ["category"] = "Fortification", ["description"] = "Container red 1", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [305] [306] = { ["type"] = "Container red 2", ["name"] = "Container red 2", ["category"] = "Fortification", ["description"] = "Container red 2", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [306] [307] = { ["type"] = "Container red 3", ["name"] = "Container red 3", ["category"] = "Fortification", ["description"] = "Container red 3", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [307] [308] = { ["type"] = "Container white", ["name"] = "Container white", ["category"] = "Fortification", ["description"] = "Container white", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [308] [309] = { ["type"] = "Container_10ft", ["name"] = "M92 Container 10ft", ["category"] = "Fortification", ["description"] = "M92 Container 10ft", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [309] [310] = { ["type"] = "Container_20ft", ["name"] = "M92 Container 20ft", ["category"] = "Fortification", ["description"] = "M92 Container 20ft", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [310] [311] = { ["type"] = "container_20ft", ["name"] = "Container 20ft", ["category"] = "Fortification", ["description"] = "Container 20ft", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [311] [312] = { ["type"] = "container_40ft", ["name"] = "Container 40ft", ["category"] = "Fortification", ["description"] = "Container 40ft", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [312] [313] = { ["type"] = "Container_40ft", ["name"] = "M92 Container 40ft", ["category"] = "Fortification", ["description"] = "M92 Container 40ft", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [313] [314] = { ["type"] = "Container_generator", ["name"] = "M92 Container generator", ["category"] = "Fortification", ["description"] = "M92 Container generator", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [314] [315] = { ["type"] = "Container_office", ["name"] = "M92 Container office", ["category"] = "Fortification", ["description"] = "M92 Container office", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [315] [316] = { ["type"] = "Container_watchtower", ["name"] = "M92 Container watchtower", ["category"] = "Fortification", ["description"] = "M92 Container watchtower", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [316] [317] = { ["type"] = "Container_watchtower_lights", ["name"] = "M92 Container watchtower lights", ["category"] = "Fortification", ["description"] = "M92 Container watchtower lights", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [317] [318] = { ["type"] = "Czech hedgehogs 1", ["name"] = "Czech hedgehogs 1", ["category"] = "Fortification", ["description"] = "Czech hedgehogs 1", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [318] [319] = { ["type"] = "Czech hedgehogs 2", ["name"] = "Czech hedgehogs 2", ["category"] = "Fortification", ["description"] = "Czech hedgehogs 2", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [319] [320] = { ["type"] = "Dragonteeth 1", ["name"] = "Dragonteeth 1", ["category"] = "Fortification", ["description"] = "Dragonteeth 1", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [320] [321] = { ["type"] = "Dragonteeth 2", ["name"] = "Dragonteeth 2", ["category"] = "Fortification", ["description"] = "Dragonteeth 2", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [321] [322] = { ["type"] = "Dragonteeth 3", ["name"] = "Dragonteeth 3", ["category"] = "Fortification", ["description"] = "Dragonteeth 3", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [322] [323] = { ["type"] = "Dragonteeth 4", ["name"] = "Dragonteeth 4", ["category"] = "Fortification", ["description"] = "Dragonteeth 4", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [323] [324] = { ["type"] = "Dragonteeth 5", ["name"] = "Dragonteeth 5", ["category"] = "Fortification", ["description"] = "Dragonteeth 5", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [324] [325] = { ["type"] = "Electric power box", ["name"] = "Electric power box", ["category"] = "Fortification", ["description"] = "Electric power box", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [325] [326] = { ["type"] = "ElevatedPlatform_down", ["name"] = "M92 Elevated Platform down", ["category"] = "Fortification", ["description"] = "M92 Elevated Platform down", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [326] [327] = { ["type"] = "ElevatedPlatform_up", ["name"] = "M92 Elevated Platform up", ["category"] = "Fortification", ["description"] = "M92 Elevated Platform up", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [327] [328] = { ["type"] = "Farm A", ["name"] = "Farm A", ["category"] = "Fortification", ["description"] = "Farm A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [328] [329] = { ["type"] = "Farm B", ["name"] = "Farm B", ["category"] = "Fortification", ["description"] = "Farm B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [329] [330] = { ["type"] = "FARP Ammo Dump Coating", ["name"] = "FARP Ammo Storage", ["category"] = "Fortification", ["description"] = "FARP Ammo Storage", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [330] [331] = { ["type"] = "FARP CP Blindage", ["name"] = "FARP Command Post", ["category"] = "Fortification", ["description"] = "FARP Command Post", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [331] [332] = { ["type"] = "FARP Fuel Depot", ["name"] = "FARP Fuel Depot", ["category"] = "Fortification", ["description"] = "FARP Fuel Depot", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [332] [333] = { ["type"] = "FARP Tent", ["name"] = "FARP Tent", ["category"] = "Fortification", ["description"] = "FARP Tent", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [333] [334] = { ["type"] = "FarpHide_Dmed", ["name"] = "FARP Hide Double Med", ["category"] = "Fortification", ["description"] = "FARP Hide Double Med", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [334] [335] = { ["type"] = "FarpHide_Dsmall", ["name"] = "FARP Hide Double Small", ["category"] = "Fortification", ["description"] = "FARP Hide Double Small", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [335] [336] = { ["type"] = "FarpHide_Med", ["name"] = "FARP Hide Single Med", ["category"] = "Fortification", ["description"] = "FARP Hide Single Med", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [336] [337] = { ["type"] = "FarpHide_small", ["name"] = "FARP Hide Single Small", ["category"] = "Fortification", ["description"] = "FARP Hide Single Small", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [337] [338] = { ["type"] = "Fire Control Bunker", ["name"] = "Fire control bunker", ["category"] = "Fortification", ["description"] = "Fire control bunker", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [338] [339] = { ["type"] = "fire_control", ["name"] = "Bunker with Fire Control Center", ["category"] = "Fortification", ["description"] = "Bunker with Fire Control Center", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [339] [340] = { ["type"] = "FireExtinguisher01", ["name"] = "M92 Fire Extinguisher 01", ["category"] = "Fortification", ["description"] = "M92 Fire Extinguisher 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [340] [341] = { ["type"] = "FireExtinguisher02", ["name"] = "M92 Fire Extinguisher 02", ["category"] = "Fortification", ["description"] = "M92 Fire Extinguisher 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [341] [342] = { ["type"] = "FireExtinguisher03", ["name"] = "M92 Fire Extinguisher 03", ["category"] = "Fortification", ["description"] = "M92 Fire Extinguisher 03", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [342] [343] = { ["type"] = "FlagPole", ["name"] = "Flag Pole", ["category"] = "Fortification", ["description"] = "Flag Pole", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [343] [344] = { ["type"] = "Freya_Shelter_Brick", ["name"] = "Freya Shelter Brick", ["category"] = "Fortification", ["description"] = "Freya Shelter Brick", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [344] [345] = { ["type"] = "Freya_Shelter_Concrete", ["name"] = "Freya Shelter Concrete", ["category"] = "Fortification", ["description"] = "Freya Shelter Concrete", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [345] [346] = { ["type"] = "Fuel tank", ["name"] = "Fuel tank", ["category"] = "Fortification", ["description"] = "Fuel tank", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [346] [347] = { ["type"] = "Garage A", ["name"] = "Garage A", ["category"] = "Fortification", ["description"] = "Garage A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [347] [348] = { ["type"] = "Garage B", ["name"] = "Garage B", ["category"] = "Fortification", ["description"] = "Garage B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [348] [349] = { ["type"] = "Garage small A", ["name"] = "Garage small A", ["category"] = "Fortification", ["description"] = "Garage small A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [349] [350] = { ["type"] = "Garage small B", ["name"] = "Garage small B", ["category"] = "Fortification", ["description"] = "Garage small B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [350] [351] = { ["type"] = "GeneratorF", ["name"] = "GeneratorF", ["category"] = "Fortification", ["description"] = "GeneratorF", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [351] [352] = { ["type"] = "Hangar A", ["name"] = "Hangar A", ["category"] = "Fortification", ["description"] = "Hangar A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [352] [353] = { ["type"] = "Hangar B", ["name"] = "Hangar B", ["category"] = "Fortification", ["description"] = "Hangar B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [353] [354] = { ["type"] = "Haystack 1", ["name"] = "Haystack 1", ["category"] = "Fortification", ["description"] = "Haystack 1", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [354] [355] = { ["type"] = "Haystack 2", ["name"] = "Haystack 2", ["category"] = "Fortification", ["description"] = "Haystack 2", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [355] [356] = { ["type"] = "Haystack 3", ["name"] = "Haystack 3", ["category"] = "Fortification", ["description"] = "Haystack 3", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [356] [357] = { ["type"] = "Haystack 4", ["name"] = "Haystack 4", ["category"] = "Fortification", ["description"] = "Haystack 4", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [357] [358] = { ["type"] = "Hemmkurvenhindernis", ["name"] = "Hemmkurvenhindernis", ["category"] = "Fortification", ["description"] = "Hemmkurvenhindernis", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [358] [359] = { ["type"] = "HESCO_generator", ["name"] = "M92 HESCO generator", ["category"] = "Fortification", ["description"] = "M92 HESCO generator", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [359] [360] = { ["type"] = "HESCO_post_1", ["name"] = "M92 HESCO post 1", ["category"] = "Fortification", ["description"] = "M92 HESCO post 1", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [360] [361] = { ["type"] = "HESCO_wallperimeter_1", ["name"] = "M92 HESCO wallperimeter 1", ["category"] = "Fortification", ["description"] = "M92 HESCO wallperimeter 1", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [361] [362] = { ["type"] = "HESCO_wallperimeter_2", ["name"] = "M92 HESCO wallperimeter 2", ["category"] = "Fortification", ["description"] = "M92 HESCO wallperimeter 2", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [362] [363] = { ["type"] = "HESCO_wallperimeter_3", ["name"] = "M92 HESCO wallperimeter 3", ["category"] = "Fortification", ["description"] = "M92 HESCO wallperimeter 3", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [363] [364] = { ["type"] = "HESCO_wallperimeter_4", ["name"] = "M92 HESCO wallperimeter 4", ["category"] = "Fortification", ["description"] = "M92 HESCO wallperimeter 4", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [364] [365] = { ["type"] = "HESCO_wallperimeter_5", ["name"] = "M92 HESCO wallperimeter 5", ["category"] = "Fortification", ["description"] = "M92 HESCO wallperimeter 5", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [365] [366] = { ["type"] = "HESCO_watchtower_1", ["name"] = "M92 HESCO watchtower 1", ["category"] = "Fortification", ["description"] = "M92 HESCO watchtower 1", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [366] [367] = { ["type"] = "HESCO_watchtower_2", ["name"] = "M92 HESCO watchtower 2", ["category"] = "Fortification", ["description"] = "M92 HESCO watchtower 2", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [367] [368] = { ["type"] = "HESCO_watchtower_3", ["name"] = "M92 HESCO watchtower 3", ["category"] = "Fortification", ["description"] = "M92 HESCO watchtower 3", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [368] [369] = { ["type"] = "house1arm", ["name"] = "Barracks armed", ["category"] = "Fortification", ["description"] = "Barracks armed", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [369] [370] = { ["type"] = "house2arm", ["name"] = "Watch tower armed", ["category"] = "Fortification", ["description"] = "Watch tower armed", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [370] [371] = { ["type"] = "houseA_arm", ["name"] = "Building armed", ["category"] = "Fortification", ["description"] = "Building armed", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [371] [372] = { ["type"] = "ip_tower", ["name"] = "IP Tower", ["category"] = "Fortification", ["description"] = "IP Tower", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [372] [373] = { ["type"] = "Jerrycan", ["name"] = "M92 Jerrycan", ["category"] = "Fortification", ["description"] = "M92 Jerrycan", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [373] [374] = { ["type"] = "Ladder", ["name"] = "M92 Ladder", ["category"] = "Fortification", ["description"] = "M92 Ladder", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [374] [375] = { ["type"] = "Landmine", ["name"] = "Landmine", ["category"] = "Fortification", ["description"] = "Landmine", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [375] [376] = { ["type"] = "LHD_LHA", ["name"] = "M92 LHD LHA", ["category"] = "Fortification", ["description"] = "M92 LHD LHA", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [376] [377] = { ["type"] = "Log posts 1", ["name"] = "Log posts 1", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "Log posts 1", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [377] [378] = { ["type"] = "Log posts 2", ["name"] = "Log posts 2", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "Log posts 2", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [378] [379] = { ["type"] = "Log posts 3", ["name"] = "Log posts 3", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "Log posts 3", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [379] [380] = { ["type"] = "Log ramps 1", ["name"] = "Log ramps 1", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "Log ramps 1", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [380] [381] = { ["type"] = "Log ramps 2", ["name"] = "Log ramps 2", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "Log ramps 2", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [381] [382] = { ["type"] = "Log ramps 3", ["name"] = "Log ramps 3", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "Log ramps 3", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [382] [383] = { ["type"] = "M32-10C_01", ["name"] = "M92 M32-10C 01", ["category"] = "Fortification", ["description"] = "M92 M32-10C 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [383] [384] = { ["type"] = "M32-10C_02", ["name"] = "M92 M32-10C 02", ["category"] = "Fortification", ["description"] = "M92 M32-10C 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [384] [385] = { ["type"] = "M32-10C_03", ["name"] = "M92 M32-10C 03", ["category"] = "Fortification", ["description"] = "M92 M32-10C 03", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [385] [386] = { ["type"] = "M32-10C_04", ["name"] = "M92 M32-10C 04", ["category"] = "Fortification", ["description"] = "M92 M32-10C 04", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [386] [387] = { ["type"] = "Military staff", ["name"] = "Military staff", ["category"] = "Fortification", ["description"] = "Military staff", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [387] [388] = { ["type"] = "MJ-1_01", ["name"] = "M92 MJ-1 01", ["category"] = "Fortification", ["description"] = "M92 MJ-1 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [388] [389] = { ["type"] = "MJ-1_02", ["name"] = "M92 MJ-1 02", ["category"] = "Fortification", ["description"] = "M92 MJ-1 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [389] [390] = { ["type"] = "NF-2_LightOff01", ["name"] = "M92 NF-2 LightOff 01", ["category"] = "Fortification", ["description"] = "M92 NF-2 LightOff 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [390] [391] = { ["type"] = "NF-2_LightOff02", ["name"] = "M92 NF-2 LightOff 02", ["category"] = "Fortification", ["description"] = "M92 NF-2 LightOff 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [391] [392] = { ["type"] = "NF-2_LightOn", ["name"] = "M92 NF-2 LightOn", ["category"] = "Fortification", ["description"] = "M92 NF-2 LightOn", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [392] [393] = { ["type"] = "Nodding_Donkey_Pump", ["name"] = "Nodding Donkey Pump", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "Nodding Donkey Pump", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [393] [394] = { ["type"] = "offshore WindTurbine", ["name"] = "Offshore Wind Turbine", ["category"] = "Fortification", ["description"] = "Offshore Wind Turbine", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [394] [395] = { ["type"] = "offshore WindTurbine2", ["name"] = "Offshore Wind Turbine 2", ["category"] = "Fortification", ["description"] = "Offshore Wind Turbine 2", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [395] [396] = { ["type"] = "Oil Barrel", ["name"] = "M92 Oil barrel", ["category"] = "Fortification", ["description"] = "M92 Oil barrel", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [396] [397] = { ["type"] = "Oil derrick", ["name"] = "Oil derrick", ["category"] = "Fortification", ["description"] = "Oil derrick", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [397] [398] = { ["type"] = "Oil platform", ["name"] = "Oil platform", ["category"] = "Fortification", ["description"] = "Oil platform", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [398] [399] = { ["type"] = "Orca", ["name"] = "Orca Whale", ["category"] = "Fortification", ["description"] = "Orca Whale", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [399] [400] = { ["type"] = "outpost", ["name"] = "Outpost", ["category"] = "Fortification", ["description"] = "Outpost", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [400] [401] = { ["type"] = "outpost_road", ["name"] = "Road outpost", ["category"] = "Fortification", ["description"] = "Road outpost", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [401] [402] = { ["type"] = "outpost_road_l", ["name"] = "Road outpost_L", ["category"] = "Fortification", ["description"] = "Road outpost_L", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [402] [403] = { ["type"] = "outpost_road_r", ["name"] = "Road outpost-R", ["category"] = "Fortification", ["description"] = "Road outpost-R", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [403] [404] = { ["type"] = "P20_01", ["name"] = "M92 P20 01", ["category"] = "Fortification", ["description"] = "M92 P20 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [404] [405] = { ["type"] = "Pile of Woods", ["name"] = "M92 Pile of woods", ["category"] = "Fortification", ["description"] = "M92 Pile of woods", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [405] [406] = { ["type"] = "Pump station", ["name"] = "Pump station", ["category"] = "Fortification", ["description"] = "Pump station", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [406] [407] = { ["type"] = "r11_volvo", ["name"] = "M92 R11 Volvo", ["category"] = "Fortification", ["description"] = "M92 R11 Volvo", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [407] [408] = { ["type"] = "Railway crossing A", ["name"] = "Railway crossing A", ["category"] = "Fortification", ["description"] = "Railway crossing A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [408] [409] = { ["type"] = "Railway crossing B", ["name"] = "Railway crossing B", ["category"] = "Fortification", ["description"] = "Railway crossing B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [409] [410] = { ["type"] = "Railway station", ["name"] = "Railway station", ["category"] = "Fortification", ["description"] = "Railway station", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [410] [411] = { ["type"] = "Red_Flag", ["name"] = "Mark Flag Red", ["category"] = "Fortification", ["description"] = "Mark Flag Red", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [411] [412] = { ["type"] = "Repair workshop", ["name"] = "Repair workshop", ["category"] = "Fortification", ["description"] = "Repair workshop", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [412] [413] = { ["type"] = "Restaurant 1", ["name"] = "Restaurant 1", ["category"] = "Fortification", ["description"] = "Restaurant 1", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [413] [414] = { ["type"] = "Revetment_x4", ["name"] = "M92 Revetment x4", ["category"] = "Fortification", ["description"] = "M92 Revetment x4", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [414] [415] = { ["type"] = "Revetment_x8", ["name"] = "M92 Revetment x8", ["category"] = "Fortification", ["description"] = "M92 Revetment x8", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [415] [416] = { ["type"] = "Sandbag_01", ["name"] = "M92 Sandbag 01", ["category"] = "Fortification", ["description"] = "M92 Sandbag 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [416] [417] = { ["type"] = "Sandbag_02", ["name"] = "M92 Sandbag 02", ["category"] = "Fortification", ["description"] = "M92 Sandbag 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [417] [418] = { ["type"] = "Sandbag_03", ["name"] = "M92 Sandbag 03", ["category"] = "Fortification", ["description"] = "M92 Sandbag 03", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [418] [419] = { ["type"] = "Sandbag_04", ["name"] = "M92 Sandbag 04", ["category"] = "Fortification", ["description"] = "M92 Sandbag 04", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [419] [420] = { ["type"] = "Sandbag_05", ["name"] = "M92 Sandbag 05", ["category"] = "Fortification", ["description"] = "M92 Sandbag 05", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [420] [421] = { ["type"] = "Sandbag_06", ["name"] = "M92 Sandbag 06", ["category"] = "Fortification", ["description"] = "M92 Sandbag 06", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [421] [422] = { ["type"] = "Sandbag_07", ["name"] = "M92 Sandbag 07", ["category"] = "Fortification", ["description"] = "M92 Sandbag 07", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [422] [423] = { ["type"] = "Sandbag_08", ["name"] = "M92 Sandbag 08", ["category"] = "Fortification", ["description"] = "M92 Sandbag 08", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [423] [424] = { ["type"] = "Sandbag_09", ["name"] = "M92 Sandbag 09", ["category"] = "Fortification", ["description"] = "M92 Sandbag 09", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [424] [425] = { ["type"] = "Sandbag_10", ["name"] = "M92 Sandbag 10", ["category"] = "Fortification", ["description"] = "M92 Sandbag 10", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [425] [426] = { ["type"] = "Sandbag_11", ["name"] = "M92 Sandbag 11", ["category"] = "Fortification", ["description"] = "M92 Sandbag 11", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [426] [427] = { ["type"] = "Sandbag_12", ["name"] = "M92 Sandbag 12", ["category"] = "Fortification", ["description"] = "M92 Sandbag 12", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [427] [428] = { ["type"] = "Sandbag_13", ["name"] = "M92 Sandbag 13", ["category"] = "Fortification", ["description"] = "M92 Sandbag 13", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [428] [429] = { ["type"] = "Sandbag_15", ["name"] = "M92 Sandbag 15", ["category"] = "Fortification", ["description"] = "M92 Sandbag 15", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [429] [430] = { ["type"] = "Sandbag_16", ["name"] = "M92 Sandbag 16", ["category"] = "Fortification", ["description"] = "M92 Sandbag 16", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [430] [431] = { ["type"] = "Sandbag_17", ["name"] = "M92 Sandbag 17", ["category"] = "Fortification", ["description"] = "M92 Sandbag 17", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [431] [432] = { ["type"] = "Sandbox", ["name"] = "Bunker 1", ["category"] = "Fortification", ["description"] = "Bunker 1", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [432] [433] = { ["type"] = "Shelter", ["name"] = "Shelter", ["category"] = "Fortification", ["description"] = "Shelter", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [433] [434] = { ["type"] = "Shelter B", ["name"] = "Shelter B", ["category"] = "Fortification", ["description"] = "Shelter B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [434] [435] = { ["type"] = "Shelter01", ["name"] = "M92 Shelter 01", ["category"] = "Fortification", ["description"] = "M92 Shelter 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [435] [436] = { ["type"] = "Shelter02", ["name"] = "M92 Shelter 02", ["category"] = "Fortification", ["description"] = "M92 Shelter 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [436] [437] = { ["type"] = "Shop", ["name"] = "Shop", ["category"] = "Fortification", ["description"] = "Shop", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [437] [438] = { ["type"] = "Siegfried Line", ["name"] = "Siegfried line", ["category"] = "Fortification", ["description"] = "Siegfried line", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [438] [439] = { ["type"] = "SK_C_28_naval_gun", ["name"] = "Gun 15cm SK C/28 Naval in Bunker", ["category"] = "Fortification", ["description"] = "Gun 15cm SK C/28 Naval in Bunker", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["AntiAir Armed Vehicles"] = true, ["NonAndLightArmoredUnits"] = true, [26] = true, ["Indirect fire"] = true, ["All"] = true, ["Ground Units"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [439] [440] = { ["type"] = "Small house 1A", ["name"] = "Small house 1A", ["category"] = "Fortification", ["description"] = "Small house 1A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [440] [441] = { ["type"] = "Small house 1A area", ["name"] = "Small house 1A area", ["category"] = "Fortification", ["description"] = "Small house 1A area", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [441] [442] = { ["type"] = "Small house 1B", ["name"] = "Small house 1B", ["category"] = "Fortification", ["description"] = "Small house 1B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [442] [443] = { ["type"] = "Small house 1B area", ["name"] = "Small house 1B area", ["category"] = "Fortification", ["description"] = "Small house 1B area", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [443] [444] = { ["type"] = "Small house 1C area", ["name"] = "Small house 1C area", ["category"] = "Fortification", ["description"] = "Small house 1C area", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [444] [445] = { ["type"] = "Small house 2C", ["name"] = "Small house 2C", ["category"] = "Fortification", ["description"] = "Small house 2C", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [445] [446] = { ["type"] = "Small werehouse 1", ["name"] = "Small warehouse 1", ["category"] = "Fortification", ["description"] = "Small warehouse 1", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [446] [447] = { ["type"] = "Small werehouse 2", ["name"] = "Small warehouse 2", ["category"] = "Fortification", ["description"] = "Small warehouse 2", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [447] [448] = { ["type"] = "Small werehouse 3", ["name"] = "Small warehouse 3", ["category"] = "Fortification", ["description"] = "Small warehouse 3", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [448] [449] = { ["type"] = "Small werehouse 4", ["name"] = "Small warehouse 4", ["category"] = "Fortification", ["description"] = "Small warehouse 4", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [449] [450] = { ["type"] = "Small_LightHouse", ["name"] = "Small_LightHouse", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "Small_LightHouse", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [450] [451] = { ["type"] = "Stanley_LightHouse", ["name"] = "Stanley LightHouse", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "Stanley LightHouse", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [451] [452] = { ["type"] = "Subsidiary structure 1", ["name"] = "Subsidiary structure 1", ["category"] = "Fortification", ["description"] = "Subsidiary structure 1", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [452] [453] = { ["type"] = "Subsidiary structure 2", ["name"] = "Subsidiary structure 2", ["category"] = "Fortification", ["description"] = "Subsidiary structure 2", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [453] [454] = { ["type"] = "Subsidiary structure 3", ["name"] = "Subsidiary structure 3", ["category"] = "Fortification", ["description"] = "Subsidiary structure 3", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [454] [455] = { ["type"] = "Subsidiary structure A", ["name"] = "Subsidiary structure A", ["category"] = "Fortification", ["description"] = "Subsidiary structure A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [455] [456] = { ["type"] = "Subsidiary structure B", ["name"] = "Subsidiary structure B", ["category"] = "Fortification", ["description"] = "Subsidiary structure B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [456] [457] = { ["type"] = "Subsidiary structure C", ["name"] = "Subsidiary structure C", ["category"] = "Fortification", ["description"] = "Subsidiary structure C", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [457] [458] = { ["type"] = "Subsidiary structure D", ["name"] = "Subsidiary structure D", ["category"] = "Fortification", ["description"] = "Subsidiary structure D", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [458] [459] = { ["type"] = "Subsidiary structure E", ["name"] = "Subsidiary structure E", ["category"] = "Fortification", ["description"] = "Subsidiary structure E", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [459] [460] = { ["type"] = "Subsidiary structure F", ["name"] = "Subsidiary structure F", ["category"] = "Fortification", ["description"] = "Subsidiary structure F", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [460] [461] = { ["type"] = "Subsidiary structure G", ["name"] = "Subsidiary structure G", ["category"] = "Fortification", ["description"] = "Subsidiary structure G", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [461] [462] = { ["type"] = "Supermarket A", ["name"] = "Supermarket A", ["category"] = "Fortification", ["description"] = "Supermarket A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [462] [463] = { ["type"] = "TACAN_beacon", ["name"] = "Beacon TACAN Portable TTS 3030", ["category"] = "Fortification", ["description"] = "Beacon TACAN Portable TTS 3030", ["attribute"] = { ["Ground Units Non Airdefence"] = true, [26] = true, [2] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["HeavyArmoredUnits"] = true, ["Fortifications"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [96] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [463] [464] = { ["type"] = "Tech combine", ["name"] = "Tech combine", ["category"] = "Fortification", ["description"] = "Tech combine", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [464] [465] = { ["type"] = "Tech hangar A", ["name"] = "Tech hangar A", ["category"] = "Fortification", ["description"] = "Tech hangar A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [465] [466] = { ["type"] = "Tent01", ["name"] = "M92 Tent 01", ["category"] = "Fortification", ["description"] = "M92 Tent 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [466] [467] = { ["type"] = "Tent02", ["name"] = "M92 Tent 02", ["category"] = "Fortification", ["description"] = "M92 Tent 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [467] [468] = { ["type"] = "Tent03", ["name"] = "M92 Tent 03", ["category"] = "Fortification", ["description"] = "M92 Tent 03", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [468] [469] = { ["type"] = "Tent04", ["name"] = "M92 Tent 04", ["category"] = "Fortification", ["description"] = "M92 Tent 04", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [469] [470] = { ["type"] = "Tent05", ["name"] = "M92 Tent 05", ["category"] = "Fortification", ["description"] = "M92 Tent 05", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [470] [471] = { ["type"] = "Tetrahydra", ["name"] = "Tetrahydra", ["category"] = "Fortification", ["description"] = "Tetrahydra", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [471] [472] = { ["type"] = "Toolbox01", ["name"] = "M92 Toolbox 01", ["category"] = "Fortification", ["description"] = "M92 Toolbox 01", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [472] [473] = { ["type"] = "Toolbox02", ["name"] = "M92 Toolbox 02", ["category"] = "Fortification", ["description"] = "M92 Toolbox 02", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [473] [474] = { ["type"] = "Tower Crane", ["name"] = "TowerCrane", ["category"] = "Fortification", ["isPutToWater"] = true, ["description"] = "TowerCrane", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [474] [475] = { ["type"] = "TugHarlan", ["name"] = "M92 Tug Harlan", ["category"] = "Fortification", ["description"] = "M92 Tug Harlan", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [475] [476] = { ["type"] = "TV tower", ["name"] = "TV tower", ["category"] = "Fortification", ["description"] = "TV tower", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [476] [477] = { ["type"] = "Twall_x1", ["name"] = "M92 Twall x1", ["category"] = "Fortification", ["description"] = "M92 Twall x1", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [477] [478] = { ["type"] = "Twall_x6", ["name"] = "M92 Twall x6", ["category"] = "Fortification", ["description"] = "M92 Twall x6", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [478] [479] = { ["type"] = "Twall_x6_3mts", ["name"] = "M92 Twall x6 3mts", ["category"] = "Fortification", ["description"] = "M92 Twall x6 3mts", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [479] [480] = { ["type"] = "warning_board_a", ["name"] = "Warning Board: Spy Cannot Escape!", ["category"] = "Fortification", ["description"] = "Warning Board: Spy Cannot Escape!", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [480] [481] = { ["type"] = "warning_board_b", ["name"] = "Warning Board: Catch Spy!", ["category"] = "Fortification", ["description"] = "Warning Board: Catch Spy!", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [481] [482] = { ["type"] = "Water tower A", ["name"] = "Water tower A", ["category"] = "Fortification", ["description"] = "Water tower A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [482] [483] = { ["type"] = "WC", ["name"] = "WC", ["category"] = "Fortification", ["description"] = "WC", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [483] [484] = { ["type"] = "White_Flag", ["name"] = "Mark Flag White", ["category"] = "Fortification", ["description"] = "Mark Flag White", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [484] [485] = { ["type"] = "White_Tyre", ["name"] = "Mark Tyre White", ["category"] = "Fortification", ["description"] = "Mark Tyre White", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [485] [486] = { ["type"] = "Windsock", ["name"] = "Windsock", ["category"] = "Fortification", ["description"] = "Windsock", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [486] [487] = { ["type"] = "WindTurbine", ["name"] = "Wind Turbine", ["category"] = "Fortification", ["description"] = "Wind Turbine", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [487] [488] = { ["type"] = "WindTurbine_11", ["name"] = "Wind Turbine 2", ["category"] = "Fortification", ["description"] = "Wind Turbine 2", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [488] [489] = { ["type"] = "Workshop A", ["name"] = "Workshop A", ["category"] = "Fortification", ["description"] = "Workshop A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [489] [490] = { ["type"] = "GrassAirfield", ["name"] = "Grass Airfield", ["category"] = "GrassAirfield", ["description"] = "Grass Airfield", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [490] [491] = { ["type"] = "Bridge", ["name"] = "Bridge", ["category"] = "GroundObject", ["description"] = "Bridge", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [491] [492] = { ["type"] = "Building", ["name"] = "Building", ["category"] = "GroundObject", ["description"] = "Building", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [492] [493] = { ["type"] = "Train", ["name"] = "Train", ["category"] = "GroundObject", ["description"] = "Train", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [493] [494] = { ["type"] = "Transport", ["name"] = "Transport", ["category"] = "GroundObject", ["description"] = "Transport", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [494] [495] = { ["air"] = true, ["type"] = "AH-1W", ["name"] = "AH-1W", ["category"] = "Helicopter", ["description"] = "AH-1W", ["attribute"] = { [1] = true, [2] = true, [6] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, [163] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [495] [496] = { ["air"] = true, ["type"] = "AH-64A", ["name"] = "AH-64A", ["category"] = "Helicopter", ["description"] = "AH-64A", ["attribute"] = { [1] = true, [2] = true, [6] = true, ["Helicopters"] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [157] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [496] [497] = { ["air"] = true, ["type"] = "AH-64D", ["name"] = "AH-64D", ["category"] = "Helicopter", ["description"] = "AH-64D", ["attribute"] = { [1] = true, [2] = true, [6] = true, [158] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [497] [498] = { ["air"] = true, ["type"] = "AH-64D_BLK_II", ["name"] = "AH-64D BLK.II", ["category"] = "Helicopter", ["description"] = "AH-64D BLK.II", ["attribute"] = { [1] = true, [2] = true, ["AFAPD"] = true, ["Refuelable"] = true, ["Helicopters"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, [300] = true, [6] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [498] [499] = { ["air"] = true, ["type"] = "CH-47D", ["name"] = "CH-47D", ["category"] = "Helicopter", ["description"] = "CH-47D", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, [159] = true, ["NonAndLightArmoredUnits"] = true, [25] = true, ["NonArmoredUnits"] = true, ["Transport helicopters"] = true, ["Helicopters"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [499] [500] = { ["air"] = true, ["type"] = "CH-47Fbl1", ["name"] = "CH-47F", ["category"] = "Helicopter", ["description"] = "CH-47F", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["Helicopters"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, [329] = true, ["All"] = true, [5] = true, ["Transport helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [500] [501] = { ["air"] = true, ["type"] = "CH-53E", ["name"] = "CH-53E", ["category"] = "Helicopter", ["description"] = "CH-53E", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["All"] = true, ["NonAndLightArmoredUnits"] = true, [25] = true, ["NonArmoredUnits"] = true, ["Transport helicopters"] = true, ["Helicopters"] = true, [160] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [501] [502] = { ["air"] = true, ["type"] = "Ka-27", ["name"] = "Ka-27", ["category"] = "Helicopter", ["description"] = "Ka-27", ["attribute"] = { [1] = true, [2] = true, [154] = true, ["Air"] = true, ["NonAndLightArmoredUnits"] = true, [25] = true, ["NonArmoredUnits"] = true, ["Transport helicopters"] = true, ["Helicopters"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [502] [503] = { ["air"] = true, ["type"] = "Ka-50", ["name"] = "Ka-50", ["category"] = "Helicopter", ["description"] = "Ka-50", ["attribute"] = { [1] = true, [2] = true, [6] = true, ["NonAndLightArmoredUnits"] = true, [155] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [503] [504] = { ["air"] = true, ["type"] = "Ka-50_3", ["name"] = "Ka-50 III", ["category"] = "Helicopter", ["description"] = "Ka-50 III", ["attribute"] = { [1] = true, [2] = true, [6] = true, [326] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [504] [505] = { ["air"] = true, ["type"] = "Mi-24P", ["name"] = "Mi-24P", ["category"] = "Helicopter", ["description"] = "Mi-24P", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["NonArmoredUnits"] = true, [296] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, ["Helicopters"] = true, ["Transports"] = true, ["All"] = true, [6] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [505] [506] = { ["air"] = true, ["type"] = "Mi-24V", ["name"] = "Mi-24V", ["category"] = "Helicopter", ["description"] = "Mi-24V", ["attribute"] = { [1] = true, [2] = true, [6] = true, ["Air"] = true, ["NonAndLightArmoredUnits"] = true, [152] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [506] [507] = { ["air"] = true, ["type"] = "Mi-26", ["name"] = "Mi-26", ["category"] = "Helicopter", ["description"] = "Mi-26", ["attribute"] = { [1] = true, [2] = true, [153] = true, [25] = true, ["NonArmoredUnits"] = true, ["Transport helicopters"] = true, ["Helicopters"] = true, ["NonAndLightArmoredUnits"] = true, ["Transports"] = true, ["All"] = true, ["Planes"] = true, ["Air"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [507] [508] = { ["air"] = true, ["type"] = "Mi-28N", ["name"] = "Mi-28N [CH]", ["category"] = "Helicopter", ["description"] = "Mi-28N [CH]", ["attribute"] = { [1] = true, [2] = true, [6] = true, ["NonAndLightArmoredUnits"] = true, [341] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [508] [509] = { ["air"] = true, ["type"] = "Mi-8MT", ["name"] = "Mi-8MTV2", ["category"] = "Helicopter", ["description"] = "Mi-8MTV2", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["Helicopters"] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, [151] = true, ["Transports"] = true, ["All"] = true, [6] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [509] [510] = { ["air"] = true, ["type"] = "OH-58D", ["name"] = "OH-58D", ["category"] = "Helicopter", ["description"] = "OH-58D", ["attribute"] = { [1] = true, [2] = true, [6] = true, [168] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [510] [511] = { ["air"] = true, ["type"] = "OH58D", ["name"] = "OH-58D(R)", ["category"] = "Helicopter", ["description"] = "OH-58D(R)", ["attribute"] = { [1] = true, [2] = true, [6] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, [336] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [511] [512] = { ["air"] = true, ["type"] = "SA342L", ["name"] = "SA342L", ["category"] = "Helicopter", ["description"] = "SA342L", ["attribute"] = { [1] = true, [2] = true, [6] = true, [290] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [512] [513] = { ["air"] = true, ["type"] = "SA342M", ["name"] = "SA342M", ["category"] = "Helicopter", ["description"] = "SA342M", ["attribute"] = { [1] = true, [2] = true, [6] = true, ["Helicopters"] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [289] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [513] [514] = { ["air"] = true, ["type"] = "SA342Minigun", ["name"] = "SA342Minigun", ["category"] = "Helicopter", ["description"] = "SA342Minigun", ["attribute"] = { [1] = true, [2] = true, [6] = true, ["NonAndLightArmoredUnits"] = true, [292] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [514] [515] = { ["air"] = true, ["type"] = "SA342Mistral", ["name"] = "SA342Mistral", ["category"] = "Helicopter", ["description"] = "SA342Mistral", ["attribute"] = { [1] = true, [2] = true, [6] = true, [291] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [515] [516] = { ["air"] = true, ["type"] = "SH-3W", ["name"] = "SH-3W", ["category"] = "Helicopter", ["description"] = "SH-3W", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["Helicopters"] = true, ["NonAndLightArmoredUnits"] = true, [25] = true, ["NonArmoredUnits"] = true, ["Transport helicopters"] = true, [164] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [516] [517] = { ["air"] = true, ["type"] = "SH-60B", ["name"] = "SH-60B", ["category"] = "Helicopter", ["description"] = "SH-60B", ["attribute"] = { [1] = true, [2] = true, [6] = true, [161] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [517] [518] = { ["air"] = true, ["type"] = "UH-1H", ["name"] = "UH-1H", ["category"] = "Helicopter", ["description"] = "UH-1H", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["Helicopters"] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, [166] = true, ["Transports"] = true, ["All"] = true, [6] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [518] [519] = { ["air"] = true, ["type"] = "UH-60A", ["name"] = "UH-60A", ["category"] = "Helicopter", ["description"] = "UH-60A", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["NonAndLightArmoredUnits"] = true, [162] = true, [25] = true, ["NonArmoredUnits"] = true, ["Transport helicopters"] = true, ["Helicopters"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [519] [520] = { ["type"] = "FARP", ["name"] = "FARP", ["category"] = "Heliport", ["isPutToWater"] = true, ["description"] = "FARP", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [520] [521] = { ["type"] = "FARP_SINGLE_01", ["name"] = "PAD Single", ["category"] = "Heliport", ["isPutToWater"] = true, ["description"] = "PAD Single", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [521] [522] = { ["type"] = "Gas platform", ["name"] = "Gas platform", ["category"] = "Heliport", ["isPutToWater"] = true, ["description"] = "Gas platform", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [522] [523] = { ["type"] = "Invisible FARP", ["name"] = "Invisible FARP", ["category"] = "Heliport", ["description"] = "Invisible FARP", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [523] [524] = { ["type"] = "Oil rig", ["name"] = "Oil rig", ["category"] = "Heliport", ["isPutToWater"] = true, ["description"] = "Oil rig", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [524] [525] = { ["type"] = "SINGLE_HELIPAD", ["name"] = "Helipad Single", ["category"] = "Heliport", ["isPutToWater"] = true, ["description"] = "Helipad Single", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [525] [526] = { ["type"] = "Infantry AK", ["name"] = "Infantry AK-74 Rus ver1", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry AK-74 Rus ver1", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, ["NonArmoredUnits"] = true, ["Skeleton_type_A"] = true, [90] = true, ["New infantry"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [526] [527] = { ["type"] = "Infantry AK Ins", ["name"] = "Insurgent AKM", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Insurgent AKM", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, ["NonArmoredUnits"] = true, ["Skeleton_type_A"] = true, [90] = true, ["New infantry"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [527] [528] = { ["type"] = "Infantry AK ver2", ["name"] = "Infantry AK-74 Rus ver2", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry AK-74 Rus ver2", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, ["NonArmoredUnits"] = true, ["Skeleton_type_A"] = true, [90] = true, ["New infantry"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [528] [529] = { ["type"] = "Infantry AK ver3", ["name"] = "Infantry AK-74 Rus ver3", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry AK-74 Rus ver3", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, ["NonArmoredUnits"] = true, ["Skeleton_type_A"] = true, [90] = true, ["New infantry"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [529] [530] = { ["type"] = "JTAC", ["name"] = "JTAC", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "JTAC", ["attribute"] = { ["Infantry"] = true, [27] = true, [2] = true, [16] = true, [90] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground Units Non Airdefence"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [530] [531] = { ["type"] = "Paratrooper AKS-74", ["name"] = "Paratrooper AKS", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Paratrooper AKS", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, [90] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [531] [532] = { ["type"] = "Paratrooper RPG-16", ["name"] = "Paratrooper RPG-16", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Paratrooper RPG-16", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, [90] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [532] [533] = { ["type"] = "Soldier AK", ["name"] = "Infantry AK-74", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry AK-74", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, [90] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [533] [534] = { ["type"] = "Soldier M249", ["name"] = "Infantry M249", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry M249", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, ["Prone"] = true, [90] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [534] [535] = { ["type"] = "Soldier M4", ["name"] = "Infantry M4", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry M4", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, [90] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [535] [536] = { ["type"] = "Soldier M4 GRG", ["name"] = "Infantry M4 Georgia", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry M4 Georgia", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, ["NonArmoredUnits"] = true, [90] = true, ["New infantry"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [536] [537] = { ["type"] = "Soldier RPG", ["name"] = "Infantry RPG", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry RPG", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, [90] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [537] [538] = { ["type"] = "soldier_mauser98", ["name"] = "Infantry Mauser 98", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry Mauser 98", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, ["NonArmoredUnits"] = true, ["Skeleton_type_A"] = true, [90] = true, ["New infantry"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [538] [539] = { ["type"] = "soldier_wwii_br_01", ["name"] = "Infantry SMLE No.4 Mk-1", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry SMLE No.4 Mk-1", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, ["NonArmoredUnits"] = true, ["Skeleton_type_A"] = true, [90] = true, ["New infantry"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [539] [540] = { ["type"] = "soldier_wwii_us", ["name"] = "Infantry M1 Garand", ["category"] = "Infantry", ["infantry"] = true, ["description"] = "Infantry M1 Garand", ["attribute"] = { ["Infantry"] = true, [26] = true, [2] = true, ["NonArmoredUnits"] = true, ["Skeleton_type_A"] = true, [90] = true, ["New infantry"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [540] [541] = { ["type"] = "DRG_Class_86", ["name"] = "Loco DRG Class 86", ["category"] = "Locomotive", ["description"] = "Loco DRG Class 86", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, [99] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [541] [542] = { ["type"] = "Electric locomotive", ["name"] = "Loco VL80 Electric", ["category"] = "Locomotive", ["description"] = "Loco VL80 Electric", ["vehicle"] = true, ["attribute"] = { ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Unarmed vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [542] [543] = { ["type"] = "ES44AH", ["name"] = "Loco ES44AH", ["category"] = "Locomotive", ["description"] = "Loco ES44AH", ["vehicle"] = true, ["attribute"] = { [48] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground Units Non Airdefence"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [543] [544] = { ["type"] = "Locomotive", ["name"] = "Loco CHME3T", ["category"] = "Locomotive", ["description"] = "Loco CHME3T", ["vehicle"] = true, ["attribute"] = { [48] = true, ["Vehicles"] = true, [100] = true, [2] = true, [8] = true, ["Trucks"] = true, ["Ground vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground Units Non Airdefence"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [544] [545] = { ["type"] = "CHAP_9K720_Cluster", ["name"] = "SRBM 9K720 Iskander CM [CH]", ["category"] = "MissilesSS", ["description"] = "SRBM 9K720 Iskander CM [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Ground vehicles"] = true, ["SS_missile"] = true, ["Armed ground units"] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [27] = true, [369] = true, ["NonAndLightArmoredUnits"] = true, ["Indirect fire"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [545] [546] = { ["type"] = "CHAP_9K720_HE", ["name"] = "SRBM 9K720 Iskander HE [CH]", ["category"] = "MissilesSS", ["description"] = "SRBM 9K720 Iskander HE [CH]", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, ["LightArmoredUnits"] = true, [17] = true, ["Ground vehicles"] = true, ["SS_missile"] = true, ["Armed ground units"] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, [368] = true, [27] = true, ["NonAndLightArmoredUnits"] = true, ["Indirect fire"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [546] [547] = { ["type"] = "hy_launcher", ["name"] = "AShM SS-N-2 Silkworm", ["category"] = "MissilesSS", ["description"] = "AShM SS-N-2 Silkworm", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [16] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, ["SS_missile"] = true, ["Armed vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [339] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [547] [548] = { ["type"] = "PL5EII Loadout", ["name"] = "Payload PL-5EII", ["category"] = "MissilesSS", ["description"] = "Payload PL-5EII", ["attribute"] = { ["Missile"] = true, [17] = true, [96] = true, ["NonArmoredUnits"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [548] [549] = { ["type"] = "PL8 Loadout", ["name"] = "Payload PL-8", ["category"] = "MissilesSS", ["description"] = "Payload PL-8", ["attribute"] = { ["Missile"] = true, [17] = true, [96] = true, ["NonArmoredUnits"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [549] [550] = { ["type"] = "Scud_B", ["name"] = "SSM SS-1C Scud-B", ["category"] = "MissilesSS", ["description"] = "SSM SS-1C Scud-B", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armed vehicles"] = true, [63] = true, [17] = true, ["Ground vehicles"] = true, ["SS_missile"] = true, ["Armed ground units"] = true, ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [27] = true, ["Indirect fire"] = true, ["All"] = true, ["Ground Units"] = true, ["Datalink"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [550] [551] = { ["type"] = "SD10 Loadout", ["name"] = "Payload SD-10", ["category"] = "MissilesSS", ["description"] = "Payload SD-10", ["attribute"] = { ["Missile"] = true, [17] = true, [96] = true, ["NonArmoredUnits"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [551] [552] = { ["type"] = "Silkworm_SR", ["name"] = "AShM Silkworm SR", ["category"] = "MissilesSS", ["description"] = "AShM Silkworm SR", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, ["Ground Units Non Airdefence"] = true, ["Vehicles"] = true, [263] = true, [2] = true, ["LightArmoredUnits"] = true, ["Armed vehicles"] = true, [101] = true, ["Ground vehicles"] = true, ["Indirect fire"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units"] = true, ["Armed ground units"] = true, ["All"] = true, ["DetectionByAWACS"] = true, [16] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [552] [553] = { ["type"] = "v1_launcher", ["name"] = "V-1 Launch Ramp", ["category"] = "MissilesSS", ["description"] = "V-1 Launch Ramp", ["vehicle"] = true, ["attribute"] = { ["Artillery"] = true, [63] = true, ["Vehicles"] = true, [27] = true, [2] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["SS_missile"] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground Units Non Airdefence"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [553] [554] = { ["type"] = "Carrier Airboss", ["name"] = "Carrier Airboss", ["category"] = "Personnel", ["description"] = "Carrier Airboss", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [554] [555] = { ["type"] = "Carrier LSO Personell", ["name"] = "Carrier LSO 1", ["category"] = "Personnel", ["description"] = "Carrier LSO 1", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [555] [556] = { ["type"] = "Carrier LSO Personell 1", ["name"] = "Carrier LSO 2", ["category"] = "Personnel", ["description"] = "Carrier LSO 2", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [556] [557] = { ["type"] = "Carrier LSO Personell 2", ["name"] = "Carrier LSO 3", ["category"] = "Personnel", ["description"] = "Carrier LSO 3", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [557] [558] = { ["type"] = "Carrier LSO Personell 3", ["name"] = "Carrier LSO 4", ["category"] = "Personnel", ["description"] = "Carrier LSO 4", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [558] [559] = { ["type"] = "Carrier LSO Personell 4", ["name"] = "Carrier LSO 5", ["category"] = "Personnel", ["description"] = "Carrier LSO 5", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [559] [560] = { ["type"] = "Carrier LSO Personell 5", ["name"] = "Carrier LSO 6", ["category"] = "Personnel", ["description"] = "Carrier LSO 6", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [560] [561] = { ["type"] = "Carrier Seaman", ["name"] = "Carrier Seaman", ["category"] = "Personnel", ["description"] = "Carrier Seaman", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [561] [562] = { ["type"] = "us carrier shooter", ["name"] = "Carrier Shooter", ["category"] = "Personnel", ["description"] = "Carrier Shooter", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [562] [563] = { ["type"] = "us carrier tech", ["name"] = "Carrier Technician", ["category"] = "Personnel", ["description"] = "Carrier Technician", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [563] [564] = { ["air"] = true, ["type"] = "A-10A", ["name"] = "A-10A", ["category"] = "Plane", ["description"] = "A-10A", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, [17] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, [6] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [564] [565] = { ["air"] = true, ["type"] = "A-10C", ["name"] = "A-10C", ["category"] = "Plane", ["description"] = "A-10C", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["Refuelable"] = true, ["Link16"] = true, ["Planes"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, [6] = true, ["All"] = true, ["Datalink"] = true, [58] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [565] [566] = { ["air"] = true, ["type"] = "A-10C_2", ["name"] = "A-10C II", ["category"] = "Plane", ["description"] = "A-10C II", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["Refuelable"] = true, ["Link16"] = true, ["Planes"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, [6] = true, ["All"] = true, ["Datalink"] = true, [264] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [566] [567] = { ["air"] = true, ["type"] = "A-20G", ["name"] = "A-20G", ["category"] = "Plane", ["description"] = "A-20G", ["attribute"] = { [1] = true, ["Air"] = true, [263] = true, ["NonArmoredUnits"] = true, [4] = true, ["Strategic bombers"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Bombers"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [567] [568] = { ["air"] = true, ["type"] = "A-50", ["name"] = "A-50", ["category"] = "Plane", ["description"] = "A-50", ["attribute"] = { [1] = true, ["Air"] = true, [26] = true, ["Refuelable"] = true, ["AWACS"] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [568] [569] = { ["air"] = true, ["type"] = "AJS37", ["name"] = "AJS37", ["category"] = "Plane", ["description"] = "AJS37", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, [265] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [569] [570] = { ["air"] = true, ["type"] = "An-26B", ["name"] = "An-26B", ["category"] = "Plane", ["description"] = "An-26B", ["attribute"] = { [1] = true, ["Air"] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, [39] = true, ["Transports"] = true, ["All"] = true, [5] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [570] [571] = { ["air"] = true, ["type"] = "An-30M", ["name"] = "An-30M", ["category"] = "Plane", ["description"] = "An-30M", ["attribute"] = { [1] = true, ["Air"] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Transports"] = true, ["All"] = true, [5] = true, [40] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [571] [572] = { ["air"] = true, ["type"] = "AV8BNA", ["name"] = "AV-8B N/A", ["category"] = "Plane", ["description"] = "AV-8B N/A", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, [266] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Bombers"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [572] [573] = { ["air"] = true, ["type"] = "B-17G", ["name"] = "B-17G", ["category"] = "Plane", ["description"] = "B-17G", ["attribute"] = { [1] = true, ["Air"] = true, [294] = true, [4] = true, ["Strategic bombers"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, ["NonArmoredUnits"] = true, ["Bombers"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [573] [574] = { ["air"] = true, ["type"] = "B-1B", ["name"] = "B-1B", ["category"] = "Plane", ["description"] = "B-1B", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["Refuelable"] = true, ["Strategic bombers"] = true, ["Bombers"] = true, ["Planes"] = true, ["Battle airplanes"] = true, [19] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Link16"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [574] [575] = { ["air"] = true, ["type"] = "B-52H", ["name"] = "B-52H", ["category"] = "Plane", ["description"] = "B-52H", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, [4] = true, ["Strategic bombers"] = true, ["Bombers"] = true, ["Planes"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Link16"] = true, ["All"] = true, [23] = true, ["Datalink"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [575] [576] = { ["air"] = true, ["type"] = "Bf-109K-4", ["name"] = "Bf 109 K-4", ["category"] = "Plane", ["description"] = "Bf 109 K-4", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, [257] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [576] [577] = { ["air"] = true, ["type"] = "C-101CC", ["name"] = "C-101CC", ["category"] = "Plane", ["description"] = "C-101CC", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [6] = true, ["Planes"] = true, [270] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [577] [578] = { ["air"] = true, ["type"] = "C-101EB", ["name"] = "C-101EB", ["category"] = "Plane", ["description"] = "C-101EB", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, [269] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["All"] = true, [6] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [578] [579] = { ["air"] = true, ["type"] = "C-130", ["name"] = "C-130", ["category"] = "Plane", ["description"] = "C-130", ["attribute"] = { [1] = true, ["Air"] = true, [31] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["Transports"] = true, ["All"] = true, [5] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [579] [580] = { ["air"] = true, ["type"] = "C-17A", ["name"] = "C-17A", ["category"] = "Plane", ["description"] = "C-17A", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, [47] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["Transports"] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [580] [581] = { ["air"] = true, ["type"] = "C-47", ["name"] = "C-47", ["category"] = "Plane", ["description"] = "C-47", ["attribute"] = { [1] = true, ["Air"] = true, ["Planes"] = true, [299] = true, ["NonArmoredUnits"] = true, ["Transports"] = true, ["All"] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [581] [582] = { ["air"] = true, ["type"] = "Christen Eagle II", ["name"] = "Christen Eagle II", ["category"] = "Plane", ["description"] = "Christen Eagle II", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, [274] = true, ["Planes"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [582] [583] = { ["air"] = true, ["type"] = "E-2C", ["name"] = "E-2D", ["category"] = "Plane", ["description"] = "E-2D", ["attribute"] = { [1] = true, ["Air"] = true, ["NonArmoredUnits"] = true, [41] = true, ["Link16"] = true, ["AWACS"] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [583] [584] = { ["air"] = true, ["type"] = "E-3A", ["name"] = "E-3A", ["category"] = "Plane", ["description"] = "E-3A", ["attribute"] = { [1] = true, ["Air"] = true, [27] = true, ["Refuelable"] = true, ["Link16"] = true, ["AWACS"] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [584] [585] = { ["air"] = true, ["type"] = "F-117A", ["name"] = "F-117A", ["category"] = "Plane", ["description"] = "F-117A", ["attribute"] = { [1] = true, ["Air"] = true, [37] = true, ["NonArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, [6] = true, ["All"] = true, ["Bombers"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [585] [586] = { ["air"] = true, ["type"] = "F-14A", ["name"] = "F-14A", ["category"] = "Plane", ["description"] = "F-14A", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonArmoredUnits"] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["All"] = true, ["Planes"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [586] [587] = { ["air"] = true, ["type"] = "F-14A-135-GR", ["name"] = "F-14A-135-GR", ["category"] = "Plane", ["description"] = "F-14A-135-GR", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, [279] = true, ["All"] = true, ["Datalink"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [587] [588] = { ["air"] = true, ["type"] = "F-14A-135-GR-Early", ["name"] = "F-14A-135-GR (Early)", ["category"] = "Plane", ["description"] = "F-14A-135-GR (Early)", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["Refuelable"] = true, ["Battle airplanes"] = true, [345] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [588] [589] = { ["air"] = true, ["type"] = "F-14B", ["name"] = "F-14B", ["category"] = "Plane", ["description"] = "F-14B", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, [278] = true, ["Refuelable"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [589] [590] = { ["air"] = true, ["type"] = "F-15C", ["name"] = "F-15C", ["category"] = "Plane", ["description"] = "F-15C", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["Refuelable"] = true, ["Link16"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, [6] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [590] [591] = { ["air"] = true, ["type"] = "F-15E", ["name"] = "F-15E", ["category"] = "Plane", ["description"] = "F-15E", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["Refuelable"] = true, ["Link16"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, [59] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [591] [592] = { ["air"] = true, ["type"] = "F-15ESE", ["name"] = "F-15E S4+", ["category"] = "Plane", ["description"] = "F-15E S4+", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["Refuelable"] = true, ["Link16"] = true, [327] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [592] [593] = { ["air"] = true, ["type"] = "F-16A", ["name"] = "F-16A", ["category"] = "Plane", ["description"] = "F-16A", ["attribute"] = { [1] = true, ["Air"] = true, [52] = true, ["Refuelable"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [593] [594] = { ["air"] = true, ["type"] = "F-16A MLU", ["name"] = "F-16A MLU", ["category"] = "Plane", ["description"] = "F-16A MLU", ["attribute"] = { [1] = true, ["Air"] = true, [52] = true, ["Refuelable"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [594] [595] = { ["air"] = true, ["type"] = "F-16C bl.50", ["name"] = "F-16C bl.50", ["category"] = "Plane", ["description"] = "F-16C bl.50", ["attribute"] = { [1] = true, ["Air"] = true, [7] = true, ["Refuelable"] = true, ["Link16"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [595] [596] = { ["air"] = true, ["type"] = "F-16C bl.52d", ["name"] = "F-16C bl.52d", ["category"] = "Plane", ["description"] = "F-16C bl.52d", ["attribute"] = { [1] = true, ["Air"] = true, [7] = true, ["Refuelable"] = true, ["Link16"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [596] [597] = { ["air"] = true, ["type"] = "F-16C_50", ["name"] = "F-16CM bl.50", ["category"] = "Plane", ["description"] = "F-16CM bl.50", ["attribute"] = { [1] = true, [275] = true, ["Refuelable"] = true, ["Link16"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["Air"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [597] [598] = { ["air"] = true, ["type"] = "F-4E", ["name"] = "F-4E", ["category"] = "Plane", ["description"] = "F-4E", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [45] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [598] [599] = { ["air"] = true, ["type"] = "F-4E-45MC", ["name"] = "F-4E-45MC", ["category"] = "Plane", ["description"] = "F-4E-45MC", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonArmoredUnits"] = true, [328] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["Refuelable"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [599] [600] = { ["air"] = true, ["type"] = "F-5E", ["name"] = "F-5E", ["category"] = "Plane", ["description"] = "F-5E", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, [46] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Battle airplanes"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [600] [601] = { ["air"] = true, ["type"] = "F-5E-3", ["name"] = "F-5E-3", ["category"] = "Plane", ["description"] = "F-5E-3", ["attribute"] = { [1] = true, [276] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Air"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [601] [602] = { ["air"] = true, ["type"] = "F-5E-3_FC", ["name"] = "F-5E FC", ["category"] = "Plane", ["description"] = "F-5E FC", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [331] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [602] [603] = { ["air"] = true, ["type"] = "F-86F Sabre", ["name"] = "F-86F", ["category"] = "Plane", ["description"] = "F-86F", ["attribute"] = { [1] = true, ["Air"] = true, [277] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Battle airplanes"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [603] [604] = { ["air"] = true, ["type"] = "F-86F_FC", ["name"] = "F-86F FC", ["category"] = "Plane", ["description"] = "F-86F FC", ["attribute"] = { [1] = true, [332] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Air"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [604] [605] = { ["air"] = true, ["type"] = "F/A-18A", ["name"] = "F/A-18A", ["category"] = "Plane", ["description"] = "F/A-18A", ["attribute"] = { [1] = true, ["Air"] = true, [14] = true, ["Refuelable"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [605] [606] = { ["air"] = true, ["type"] = "F/A-18C", ["name"] = "F/A-18C", ["category"] = "Plane", ["description"] = "F/A-18C", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, ["Link16"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [53] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [606] [607] = { ["air"] = true, ["type"] = "F4U-1D", ["name"] = "F4U-1D", ["category"] = "Plane", ["description"] = "F4U-1D", ["attribute"] = { [1] = true, [339] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Air"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [607] [608] = { ["air"] = true, ["type"] = "F4U-1D_CW", ["name"] = "F4U-1D Mk.IV", ["category"] = "Plane", ["description"] = "F4U-1D Mk.IV", ["attribute"] = { [1] = true, ["Air"] = true, [340] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [608] [609] = { ["air"] = true, ["type"] = "FA-18C_hornet", ["name"] = "F/A-18C Lot 20", ["category"] = "Plane", ["description"] = "F/A-18C Lot 20", ["attribute"] = { [1] = true, ["Link4"] = true, ["Refuelable"] = true, ["ACLS"] = true, ["Link16"] = true, ["Multirole fighters"] = true, ["Planes"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, [280] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [609] [610] = { ["air"] = true, ["type"] = "Falcon_Gyrocopter", ["name"] = "Falcon Assault Gyrocopter", ["category"] = "Plane", ["description"] = "Falcon Assault Gyrocopter", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["Battle airplanes"] = true, [330] = true, ["Planes"] = true, ["Transports"] = true, ["All"] = true, [6] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [610] [611] = { ["air"] = true, ["type"] = "FW-190A8", ["name"] = "Fw 190 A-8", ["category"] = "Plane", ["description"] = "Fw 190 A-8", ["attribute"] = { [1] = true, ["Air"] = true, [256] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Battle airplanes"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [611] [612] = { ["air"] = true, ["type"] = "FW-190D9", ["name"] = "Fw 190 D-9", ["category"] = "Plane", ["description"] = "Fw 190 D-9", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [255] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [612] [613] = { ["air"] = true, ["type"] = "H-6J", ["name"] = "H-6J", ["category"] = "Plane", ["description"] = "H-6J", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["Strategic bombers"] = true, [298] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Bombers"] = true, ["All"] = true, ["Datalink"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [613] [614] = { ["air"] = true, ["type"] = "Hawk", ["name"] = "Hawk", ["category"] = "Plane", ["description"] = "Hawk", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, [281] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [614] [615] = { ["air"] = true, ["type"] = "I-16", ["name"] = "I-16", ["category"] = "Plane", ["description"] = "I-16", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [282] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [615] [616] = { ["air"] = true, ["type"] = "IL-76MD", ["name"] = "IL-76MD", ["category"] = "Plane", ["description"] = "IL-76MD", ["attribute"] = { [1] = true, ["Air"] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Transports"] = true, ["All"] = true, [5] = true, [30] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [616] [617] = { ["air"] = true, ["type"] = "IL-78M", ["name"] = "IL-78M", ["category"] = "Plane", ["description"] = "IL-78M", ["attribute"] = { [1] = true, ["Air"] = true, ["Tankers"] = true, [28] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [617] [618] = { ["air"] = true, ["type"] = "J-11A", ["name"] = "J-11A", ["category"] = "Plane", ["description"] = "J-11A", ["attribute"] = { [1] = true, ["Air"] = true, [66] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [618] [619] = { ["air"] = true, ["type"] = "JF-17", ["name"] = "JF-17", ["category"] = "Plane", ["description"] = "JF-17", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, ["Link16"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [271] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [619] [620] = { ["air"] = true, ["type"] = "Ju-88A4", ["name"] = "Ju 88 A-4", ["category"] = "Plane", ["description"] = "Ju 88 A-4", ["attribute"] = { [1] = true, ["Air"] = true, ["NonArmoredUnits"] = true, [295] = true, ["Strategic bombers"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, ["Bombers"] = true, [4] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [620] [621] = { ["air"] = true, ["type"] = "KC-135", ["name"] = "KC-135", ["category"] = "Plane", ["description"] = "KC-135", ["attribute"] = { [1] = true, ["Air"] = true, ["Tankers"] = true, ["Refuelable"] = true, ["Link16"] = true, [60] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [621] [622] = { ["air"] = true, ["type"] = "KC130", ["name"] = "KC-130", ["category"] = "Plane", ["description"] = "KC-130", ["attribute"] = { [1] = true, ["Air"] = true, ["Tankers"] = true, ["Refuelable"] = true, [267] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [622] [623] = { ["air"] = true, ["type"] = "KC135MPRS", ["name"] = "KC-135MPRS", ["category"] = "Plane", ["description"] = "KC-135MPRS", ["attribute"] = { [1] = true, ["Air"] = true, ["Tankers"] = true, ["Refuelable"] = true, ["Link16"] = true, [268] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [623] [624] = { ["air"] = true, ["type"] = "KJ-2000", ["name"] = "KJ-2000", ["category"] = "Plane", ["description"] = "KJ-2000", ["attribute"] = { [1] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["Link16"] = true, ["AWACS"] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, [272] = true, ["All"] = true, ["Datalink"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [624] [625] = { ["air"] = true, ["type"] = "L-39C", ["name"] = "L-39C", ["category"] = "Plane", ["description"] = "L-39C", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, [283] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [6] = true, ["Planes"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [625] [626] = { ["air"] = true, ["type"] = "L-39ZA", ["name"] = "L-39ZA", ["category"] = "Plane", ["description"] = "L-39ZA", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [6] = true, ["All"] = true, ["Planes"] = true, [61] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [626] [627] = { ["air"] = true, ["type"] = "M-2000C", ["name"] = "M-2000C", ["category"] = "Plane", ["description"] = "M-2000C", ["attribute"] = { [1] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["Multirole fighters"] = true, [284] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["All"] = true, ["Planes"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [627] [628] = { ["air"] = true, ["type"] = "MB-339A", ["name"] = "MB-339A", ["category"] = "Plane", ["description"] = "MB-339A", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [324] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [628] [629] = { ["air"] = true, ["type"] = "MB-339APAN", ["name"] = "MB-339A/PAN", ["category"] = "Plane", ["description"] = "MB-339A/PAN", ["attribute"] = { [1] = true, [325] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Air"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [629] [630] = { ["air"] = true, ["type"] = "MiG-15bis", ["name"] = "MiG-15bis", ["category"] = "Plane", ["description"] = "MiG-15bis", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, [286] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [630] [631] = { ["air"] = true, ["type"] = "MiG-15bis_FC", ["name"] = "MiG-15bis FC", ["category"] = "Plane", ["description"] = "MiG-15bis FC", ["attribute"] = { [1] = true, ["Air"] = true, [333] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Battle airplanes"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [631] [632] = { ["air"] = true, ["type"] = "MiG-19P", ["name"] = "MiG-19P", ["category"] = "Plane", ["description"] = "MiG-19P", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, [287] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [632] [633] = { ["air"] = true, ["type"] = "MiG-21Bis", ["name"] = "MiG-21Bis", ["category"] = "Plane", ["description"] = "MiG-21Bis", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, [288] = true, ["Planes"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [633] [634] = { ["air"] = true, ["type"] = "MiG-23MLD", ["name"] = "MiG-23MLD", ["category"] = "Plane", ["description"] = "MiG-23MLD", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Battle airplanes"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [634] [635] = { ["air"] = true, ["type"] = "MiG-25PD", ["name"] = "MiG-25PD", ["category"] = "Plane", ["description"] = "MiG-25PD", ["attribute"] = { [1] = true, [24] = true, ["Interceptors"] = true, ["NonArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [3] = true, ["Planes"] = true, ["Air"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [635] [636] = { ["air"] = true, ["type"] = "MiG-25RBT", ["name"] = "MiG-25RBT", ["category"] = "Plane", ["description"] = "MiG-25RBT", ["attribute"] = { [1] = true, ["Air"] = true, ["Planes"] = true, [8] = true, [3] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["NonAndLightArmoredUnits"] = true, ["Aux"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [636] [637] = { ["air"] = true, ["type"] = "MiG-27K", ["name"] = "MiG-27K", ["category"] = "Plane", ["description"] = "MiG-27K", ["attribute"] = { [1] = true, [11] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Air"] = true, ["Bombers"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [637] [638] = { ["air"] = true, ["type"] = "MiG-29 Fulcrum", ["name"] = "MiG-29A Fulcrum", ["category"] = "Plane", ["description"] = "MiG-29A Fulcrum", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, [343] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [638] [639] = { ["air"] = true, ["type"] = "MiG-29A", ["name"] = "MiG-29A", ["category"] = "Plane", ["description"] = "MiG-29A", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Battle airplanes"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [639] [640] = { ["air"] = true, ["type"] = "MiG-29G", ["name"] = "MiG-29G", ["category"] = "Plane", ["description"] = "MiG-29G", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [49] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [640] [641] = { ["air"] = true, ["type"] = "MiG-29S", ["name"] = "MiG-29S", ["category"] = "Plane", ["description"] = "MiG-29S", ["attribute"] = { [1] = true, [50] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Air"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [641] [642] = { ["air"] = true, ["type"] = "MiG-31", ["name"] = "MiG-31", ["category"] = "Plane", ["description"] = "MiG-31", ["attribute"] = { [1] = true, ["Air"] = true, ["Interceptors"] = true, ["Refuelable"] = true, [9] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [3] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [642] [643] = { ["air"] = true, ["type"] = "Mirage 2000-5", ["name"] = "Mirage 2000-5", ["category"] = "Plane", ["description"] = "Mirage 2000-5", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, ["Link16"] = true, ["Multirole fighters"] = true, [34] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [643] [644] = { ["air"] = true, ["type"] = "Mirage-F1AD", ["name"] = "Mirage F1AD", ["category"] = "Plane", ["description"] = "Mirage F1AD", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, [334] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [644] [645] = { ["air"] = true, ["type"] = "Mirage-F1AZ", ["name"] = "Mirage F1AZ", ["category"] = "Plane", ["description"] = "Mirage F1AZ", ["attribute"] = { [1] = true, [335] = true, ["NonArmoredUnits"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["All"] = true, ["Planes"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [645] [646] = { ["air"] = true, ["type"] = "Mirage-F1B", ["name"] = "Mirage F1B", ["category"] = "Plane", ["description"] = "Mirage F1B", ["attribute"] = { [1] = true, ["Air"] = true, [319] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [646] [647] = { ["air"] = true, ["type"] = "Mirage-F1BD", ["name"] = "Mirage F1BD", ["category"] = "Plane", ["description"] = "Mirage F1BD", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, [322] = true, ["All"] = true, ["NonArmoredUnits"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [647] [648] = { ["air"] = true, ["type"] = "Mirage-F1BE", ["name"] = "Mirage F1BE", ["category"] = "Plane", ["description"] = "Mirage F1BE", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [320] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [648] [649] = { ["air"] = true, ["type"] = "Mirage-F1BQ", ["name"] = "Mirage F1BQ", ["category"] = "Plane", ["description"] = "Mirage F1BQ", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [321] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [649] [650] = { ["air"] = true, ["type"] = "Mirage-F1C", ["name"] = "Mirage F1C", ["category"] = "Plane", ["description"] = "Mirage F1C", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, [301] = true, ["All"] = true, ["NonArmoredUnits"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [650] [651] = { ["air"] = true, ["type"] = "Mirage-F1C-200", ["name"] = "Mirage F1C-200", ["category"] = "Plane", ["description"] = "Mirage F1C-200", ["attribute"] = { [1] = true, ["Air"] = true, [306] = true, ["Refuelable"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [651] [652] = { ["air"] = true, ["type"] = "Mirage-F1CE", ["name"] = "Mirage F1CE", ["category"] = "Plane", ["description"] = "Mirage F1CE", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [302] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [652] [653] = { ["air"] = true, ["type"] = "Mirage-F1CG", ["name"] = "Mirage F1CG", ["category"] = "Plane", ["description"] = "Mirage F1CG", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [310] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [653] [654] = { ["air"] = true, ["type"] = "Mirage-F1CH", ["name"] = "Mirage F1CH", ["category"] = "Plane", ["description"] = "Mirage F1CH", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, [308] = true, ["All"] = true, ["NonArmoredUnits"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [654] [655] = { ["air"] = true, ["type"] = "Mirage-F1CJ", ["name"] = "Mirage F1CJ", ["category"] = "Plane", ["description"] = "Mirage F1CJ", ["attribute"] = { [1] = true, ["Air"] = true, [312] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [655] [656] = { ["air"] = true, ["type"] = "Mirage-F1CK", ["name"] = "Mirage F1CK", ["category"] = "Plane", ["description"] = "Mirage F1CK", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, [313] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["NonAndLightArmoredUnits"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [656] [657] = { ["air"] = true, ["type"] = "Mirage-F1CR", ["name"] = "Mirage F1CR", ["category"] = "Plane", ["description"] = "Mirage F1CR", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [317] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [657] [658] = { ["air"] = true, ["type"] = "Mirage-F1CT", ["name"] = "Mirage F1CT", ["category"] = "Plane", ["description"] = "Mirage F1CT", ["attribute"] = { [1] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [318] = true, ["All"] = true, ["Planes"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [658] [659] = { ["air"] = true, ["type"] = "Mirage-F1CZ", ["name"] = "Mirage F1CZ", ["category"] = "Plane", ["description"] = "Mirage F1CZ", ["attribute"] = { [1] = true, [311] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Air"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [659] [660] = { ["air"] = true, ["type"] = "Mirage-F1DDA", ["name"] = "Mirage F1DDA", ["category"] = "Plane", ["description"] = "Mirage F1DDA", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [323] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [660] [661] = { ["air"] = true, ["type"] = "Mirage-F1ED", ["name"] = "Mirage F1ED", ["category"] = "Plane", ["description"] = "Mirage F1ED", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, [315] = true, ["All"] = true, ["NonArmoredUnits"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [661] [662] = { ["air"] = true, ["type"] = "Mirage-F1EDA", ["name"] = "Mirage F1EDA", ["category"] = "Plane", ["description"] = "Mirage F1EDA", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, [316] = true, ["All"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [662] [663] = { ["air"] = true, ["type"] = "Mirage-F1EE", ["name"] = "Mirage F1EE", ["category"] = "Plane", ["description"] = "Mirage F1EE", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, [303] = true, ["NonArmoredUnits"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [663] [664] = { ["air"] = true, ["type"] = "Mirage-F1EH", ["name"] = "Mirage F1EH", ["category"] = "Plane", ["description"] = "Mirage F1EH", ["attribute"] = { [1] = true, ["Air"] = true, [307] = true, ["NonArmoredUnits"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [664] [665] = { ["air"] = true, ["type"] = "Mirage-F1EQ", ["name"] = "Mirage F1EQ", ["category"] = "Plane", ["description"] = "Mirage F1EQ", ["attribute"] = { [1] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["Multirole fighters"] = true, [314] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["All"] = true, ["Planes"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [665] [666] = { ["air"] = true, ["type"] = "Mirage-F1JA", ["name"] = "Mirage F1JA", ["category"] = "Plane", ["description"] = "Mirage F1JA", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, [309] = true, ["All"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [666] [667] = { ["air"] = true, ["type"] = "Mirage-F1M-CE", ["name"] = "Mirage F1M (C.14 1-25/32-51)", ["category"] = "Plane", ["description"] = "Mirage F1M (C.14 1-25/32-51)", ["attribute"] = { [1] = true, ["Air"] = true, [305] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [667] [668] = { ["air"] = true, ["type"] = "Mirage-F1M-EE", ["name"] = "Mirage F1M (C.14 52-73)", ["category"] = "Plane", ["description"] = "Mirage F1M (C.14 52-73)", ["attribute"] = { [1] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["Multirole fighters"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, [304] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [668] [669] = { ["air"] = true, ["type"] = "MosquitoFBMkVI", ["name"] = "Mosquito FB Mk. VI", ["category"] = "Plane", ["description"] = "Mosquito FB Mk. VI", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, [297] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, [6] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [669] [670] = { ["air"] = true, ["type"] = "MQ-9 Reaper", ["name"] = "MQ-9 Reaper", ["category"] = "Plane", ["description"] = "MQ-9 Reaper", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["UAVs"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, [285] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [670] [671] = { ["air"] = true, ["type"] = "P-47D-30", ["name"] = "P-47D-30", ["category"] = "Plane", ["description"] = "P-47D-30", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, [260] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [671] [672] = { ["air"] = true, ["type"] = "P-47D-30bl1", ["name"] = "P-47D-30 (Early)", ["category"] = "Plane", ["description"] = "P-47D-30 (Early)", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [261] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [672] [673] = { ["air"] = true, ["type"] = "P-47D-40", ["name"] = "P-47D-40", ["category"] = "Plane", ["description"] = "P-47D-40", ["attribute"] = { [1] = true, [262] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Air"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [673] [674] = { ["air"] = true, ["type"] = "P-51D", ["name"] = "P-51D-25-NA", ["category"] = "Plane", ["description"] = "P-51D-25-NA", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [63] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [674] [675] = { ["air"] = true, ["type"] = "P-51D-30-NA", ["name"] = "P-51D-30-NA", ["category"] = "Plane", ["description"] = "P-51D-30-NA", ["attribute"] = { [1] = true, [64] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Air"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [675] [676] = { ["air"] = true, ["type"] = "QF-4E", ["name"] = "QF-4E", ["category"] = "Plane", ["description"] = "QF-4E", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonArmoredUnits"] = true, [344] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["All"] = true, ["Planes"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [676] [677] = { ["air"] = true, ["type"] = "RQ-1A Predator", ["name"] = "MQ-1A Predator", ["category"] = "Plane", ["description"] = "MQ-1A Predator", ["attribute"] = { [1] = true, ["UAVs"] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["All"] = true, [55] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [677] [678] = { ["air"] = true, ["type"] = "S-3B", ["name"] = "S-3B", ["category"] = "Plane", ["description"] = "S-3B", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, ["Aux"] = true, ["Link16"] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, [42] = true, ["Datalink"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [678] [679] = { ["air"] = true, ["type"] = "S-3B Tanker", ["name"] = "S-3B Tanker", ["category"] = "Plane", ["description"] = "S-3B Tanker", ["attribute"] = { [1] = true, ["Air"] = true, [33] = true, ["Refuelable"] = true, ["Aux"] = true, ["Link16"] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Tankers"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [679] [680] = { ["air"] = true, ["type"] = "SpitfireLFMkIX", ["name"] = "Spitfire LF Mk. IX", ["category"] = "Plane", ["description"] = "Spitfire LF Mk. IX", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, [258] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [680] [681] = { ["air"] = true, ["type"] = "SpitfireLFMkIXCW", ["name"] = "Spitfire LF Mk. IX CW", ["category"] = "Plane", ["description"] = "Spitfire LF Mk. IX CW", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, [259] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [681] [682] = { ["air"] = true, ["type"] = "Su-17M4", ["name"] = "Su-17M4", ["category"] = "Plane", ["description"] = "Su-17M4", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["Bombers"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["NonAndLightArmoredUnits"] = true, [48] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [682] [683] = { ["air"] = true, ["type"] = "Su-24M", ["name"] = "Su-24M", ["category"] = "Plane", ["description"] = "Su-24M", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["Bombers"] = true, ["All"] = true, [12] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [683] [684] = { ["air"] = true, ["type"] = "Su-24MR", ["name"] = "Su-24MR", ["category"] = "Plane", ["description"] = "Su-24MR", ["attribute"] = { [1] = true, ["Air"] = true, [51] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["Refuelable"] = true, ["All"] = true, ["NonArmoredUnits"] = true, ["Aux"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [684] [685] = { ["air"] = true, ["type"] = "Su-25", ["name"] = "Su-25", ["category"] = "Plane", ["description"] = "Su-25", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, [16] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, [6] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [685] [686] = { ["air"] = true, ["type"] = "Su-25T", ["name"] = "Su-25T", ["category"] = "Plane", ["description"] = "Su-25T", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [6] = true, ["All"] = true, ["Planes"] = true, [54] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [686] [687] = { ["air"] = true, ["type"] = "Su-25TM", ["name"] = "Su-25TM", ["category"] = "Plane", ["description"] = "Su-25TM", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [38] = true, ["All"] = true, [6] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [687] [688] = { ["air"] = true, ["type"] = "Su-27", ["name"] = "Su-27", ["category"] = "Plane", ["description"] = "Su-27", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonAndLightArmoredUnits"] = true, [3] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Battle airplanes"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [688] [689] = { ["air"] = true, ["type"] = "Su-30", ["name"] = "Su-30", ["category"] = "Plane", ["description"] = "Su-30", ["attribute"] = { [1] = true, ["Air"] = true, [13] = true, ["NonAndLightArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Multirole fighters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [689] [690] = { ["air"] = true, ["type"] = "Su-33", ["name"] = "Su-33", ["category"] = "Plane", ["description"] = "Su-33", ["attribute"] = { [1] = true, ["Air"] = true, ["Fighters"] = true, ["NonArmoredUnits"] = true, [4] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [690] [691] = { ["air"] = true, ["type"] = "Su-34", ["name"] = "Su-34", ["category"] = "Plane", ["description"] = "Su-34", ["attribute"] = { [1] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, [20] = true, ["Planes"] = true, ["All"] = true, ["Bombers"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [691] [692] = { ["air"] = true, ["type"] = "TF-51D", ["name"] = "TF-51D", ["category"] = "Plane", ["description"] = "TF-51D", ["attribute"] = { [1] = true, ["Air"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Battleplanes"] = true, [65] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [692] [693] = { ["air"] = true, ["type"] = "Tornado GR4", ["name"] = "Tornado GR4", ["category"] = "Plane", ["description"] = "Tornado GR4", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, ["Link16"] = true, ["Planes"] = true, ["Battle airplanes"] = true, [10] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["Bombers"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "Tornado GR3", }, -- end of ["aliases"] }, -- end of [693] [694] = { ["air"] = true, ["type"] = "Tornado IDS", ["name"] = "Tornado IDS", ["category"] = "Plane", ["description"] = "Tornado IDS", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, [56] = true, ["Link16"] = true, ["Battle airplanes"] = true, ["Bombers"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Datalink"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [694] [695] = { ["air"] = true, ["type"] = "Tu-142", ["name"] = "Tu-142", ["category"] = "Plane", ["description"] = "Tu-142", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, [4] = true, ["Strategic bombers"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Bombers"] = true, ["Planes"] = true, [22] = true, ["NonArmoredUnits"] = true, ["All"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [695] [696] = { ["air"] = true, ["type"] = "Tu-160", ["name"] = "Tu-160", ["category"] = "Plane", ["description"] = "Tu-160", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["NonArmoredUnits"] = true, ["Strategic bombers"] = true, [18] = true, ["NonAndLightArmoredUnits"] = true, ["Bombers"] = true, ["Battle airplanes"] = true, ["All"] = true, ["Planes"] = true, ["Refuelable"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [696] [697] = { ["air"] = true, ["type"] = "Tu-22M3", ["name"] = "Tu-22M3", ["category"] = "Plane", ["description"] = "Tu-22M3", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["Battle airplanes"] = true, ["Bombers"] = true, [25] = true, ["NonArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [697] [698] = { ["air"] = true, ["type"] = "Tu-95MS", ["name"] = "Tu-95MS [CH]", ["category"] = "Plane", ["description"] = "Tu-95MS [CH]", ["attribute"] = { [1] = true, [2] = true, ["Air"] = true, ["Refuelable"] = true, ["Strategic bombers"] = true, [342] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Bombers"] = true, ["Planes"] = true, ["All"] = true, ["Datalink"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [698] [699] = { ["air"] = true, ["type"] = "WingLoong-I", ["name"] = "WingLoong-I", ["category"] = "Plane", ["description"] = "WingLoong-I", ["attribute"] = { [1] = true, ["Air"] = true, ["Battleplanes"] = true, ["NonArmoredUnits"] = true, ["UAVs"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Planes"] = true, ["All"] = true, [6] = true, [273] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [699] [700] = { ["air"] = true, ["type"] = "Yak-40", ["name"] = "Yak-40", ["category"] = "Plane", ["description"] = "Yak-40", ["attribute"] = { [1] = true, [57] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["Transports"] = true, ["All"] = true, [5] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [700] [701] = { ["air"] = true, ["type"] = "Yak-52", ["name"] = "Yak-52", ["category"] = "Plane", ["description"] = "Yak-52", ["attribute"] = { [1] = true, ["UAVs"] = true, ["Planes"] = true, ["NonAndLightArmoredUnits"] = true, [293] = true, ["NonArmoredUnits"] = true, ["All"] = true, [5] = true, ["Air"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [701] [702] = { ["type"] = "ALBATROS", ["name"] = "Corvette 1124.4 Grisha", ["category"] = "Ship", ["naval"] = true, ["description"] = "Corvette 1124.4 Grisha", ["attribute"] = { ["Heavy armed ships"] = true, [14] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, ["Frigates"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Naval"] = true, [11] = true, ["Armed Ship"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["All"] = true, [12] = true, [3] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [702] [703] = { ["type"] = "ara_vdm", ["name"] = "ARA Veinticinco de Mayo", ["category"] = "Ship", ["naval"] = true, ["description"] = "ARA Veinticinco de Mayo", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier"] = true, ["Arresting Gear"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, [343] = true, [3] = true, [12] = true, ["AircraftCarrier With Catapult"] = true, ["Heavy armed ships"] = true, ["Naval"] = true, ["Armed ships"] = true, ["Ships"] = true, ["All"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["Armed Air Defence"] = true, ["catapult"] = true, ["AircraftCarrier With Arresting Gear"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [703] [704] = { ["type"] = "atconveyor", ["name"] = "SS Atlantic Conveyor", ["category"] = "Ship", ["naval"] = true, ["description"] = "SS Atlantic Conveyor", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, ["Aircraft Carriers"] = true, ["Straight_in_approach_type"] = true, ["AircraftCarrier"] = true, ["Ships"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["All"] = true, ["Naval"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, [348] = true, ["DetectionByAWACS"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [704] [705] = { ["type"] = "BDK-775", ["name"] = "LS Ropucha", ["category"] = "Ship", ["naval"] = true, ["description"] = "LS Ropucha", ["attribute"] = { ["HeavyArmoredUnits"] = true, [14] = true, ["Armed Air Defence"] = true, ["Ships"] = true, [341] = true, ["Landing Ships"] = true, ["Armed ships"] = true, ["All"] = true, ["Naval"] = true, ["NO_SAM"] = true, [3] = true, ["Heavy armed ships"] = true, [12] = true, ["Armed Ship"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [705] [706] = { ["type"] = "CastleClass_01", ["name"] = "Castle Class", ["category"] = "Ship", ["naval"] = true, ["description"] = "Castle Class", ["attribute"] = { ["Heavy armed ships"] = true, [14] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["Naval"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, ["Corvettes"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [706] [707] = { ["type"] = "CHAP_Project22160", ["name"] = "Project 22160 Patrol Ship [CH]", ["category"] = "Ship", ["naval"] = true, ["description"] = "Project 22160 Patrol Ship [CH]", ["attribute"] = { ["Straight_in_approach_type"] = true, ["Ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, ["Naval"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Datalink"] = true, [14] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Corvettes"] = true, [371] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [707] [708] = { ["type"] = "CHAP_Project22160_TorM2KM", ["name"] = "Project 22160 Patrol Ship with Tor M2KM [CH]", ["category"] = "Ship", ["naval"] = true, ["description"] = "Project 22160 Patrol Ship with Tor M2KM [CH]", ["attribute"] = { ["Ships"] = true, [372] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, ["Naval"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, [14] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Corvettes"] = true, ["Datalink"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [708] [709] = { ["type"] = "CV_1143_5", ["name"] = "CV 1143.5 Admiral Kuznetsov(2017)", ["category"] = "Ship", ["naval"] = true, ["description"] = "CV 1143.5 Admiral Kuznetsov(2017)", ["attribute"] = { [255] = true, ["Aircraft Carriers"] = true, ["AircraftCarrier With Tramplin"] = true, ["AircraftCarrier"] = true, ["Arresting Gear"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["DetectionByAWACS"] = true, ["ski_jump"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, ["Straight_in_approach_type"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Ships"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, ["AircraftCarrier With Arresting Gear"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [709] [710] = { ["type"] = "CVN_71", ["name"] = "CVN-71 Theodore Roosevelt", ["category"] = "Ship", ["naval"] = true, ["description"] = "CVN-71 Theodore Roosevelt", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier"] = true, ["ACLS"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, [265] = true, ["AircraftCarrier With Arresting Gear"] = true, ["Link4"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, ["AircraftCarrier With Catapult"] = true, ["Naval"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Ships"] = true, ["Arresting Gear"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["catapult"] = true, ["Datalink"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [710] [711] = { ["type"] = "CVN_72", ["name"] = "CVN-72 Abraham Lincoln", ["category"] = "Ship", ["naval"] = true, ["description"] = "CVN-72 Abraham Lincoln", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier"] = true, ["ACLS"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["AircraftCarrier With Arresting Gear"] = true, ["Link4"] = true, [3] = true, ["Heavy armed ships"] = true, [12] = true, ["Arresting Gear"] = true, ["AircraftCarrier With Catapult"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Ships"] = true, [266] = true, ["Naval"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["catapult"] = true, ["Datalink"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [711] [712] = { ["type"] = "CVN_73", ["name"] = "CVN-73 George Washington", ["category"] = "Ship", ["naval"] = true, ["description"] = "CVN-73 George Washington", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier"] = true, ["ACLS"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["AircraftCarrier With Arresting Gear"] = true, ["Link4"] = true, [3] = true, ["Heavy armed ships"] = true, [12] = true, ["Arresting Gear"] = true, ["AircraftCarrier With Catapult"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Ships"] = true, ["Datalink"] = true, ["Naval"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["catapult"] = true, [267] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [712] [713] = { ["type"] = "CVN_75", ["name"] = "CVN-75 Harry S. Truman", ["category"] = "Ship", ["naval"] = true, ["description"] = "CVN-75 Harry S. Truman", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier"] = true, ["ACLS"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["AircraftCarrier With Arresting Gear"] = true, ["Link4"] = true, [3] = true, ["Heavy armed ships"] = true, [12] = true, ["Naval"] = true, ["AircraftCarrier With Catapult"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Ships"] = true, [268] = true, ["Arresting Gear"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["catapult"] = true, ["Datalink"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [713] [714] = { ["type"] = "Dry-cargo ship-1", ["name"] = "Bulker Yakushev", ["category"] = "Ship", ["naval"] = true, ["description"] = "Bulker Yakushev", ["attribute"] = { [15] = true, ["Unarmed ships"] = true, [5] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, [3] = true, ["All"] = true, [12] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [714] [715] = { ["type"] = "Dry-cargo ship-2", ["name"] = "Cargo Ivanov", ["category"] = "Ship", ["naval"] = true, ["description"] = "Cargo Ivanov", ["attribute"] = { [15] = true, ["Unarmed ships"] = true, [5] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, [3] = true, ["All"] = true, [12] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [715] [716] = { ["type"] = "ELNYA", ["name"] = "Tanker Elnya 160", ["category"] = "Ship", ["naval"] = true, ["description"] = "Tanker Elnya 160", ["attribute"] = { [15] = true, ["Unarmed ships"] = true, [5] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, [3] = true, ["All"] = true, [12] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [716] [717] = { ["type"] = "Essex", ["name"] = "Essex Class Carrier 1944", ["category"] = "Ship", ["naval"] = true, ["description"] = "Essex Class Carrier 1944", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier"] = true, ["Ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["NO_SAM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["ski_jump"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, ["Arresting Gear"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, [373] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [717] [718] = { ["type"] = "Forrestal", ["name"] = "CV-59 Forrestal", ["category"] = "Ship", ["naval"] = true, ["description"] = "CV-59 Forrestal", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier"] = true, ["ACLS"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["AircraftCarrier With Arresting Gear"] = true, ["Link4"] = true, [3] = true, ["Heavy armed ships"] = true, [12] = true, ["Arresting Gear"] = true, ["AircraftCarrier With Catapult"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Ships"] = true, ["Datalink"] = true, ["Naval"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["catapult"] = true, [315] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [718] [719] = { ["type"] = "HandyWind", ["name"] = "Bulker Handy Wind", ["category"] = "Ship", ["naval"] = true, ["description"] = "Bulker Handy Wind", ["attribute"] = { [15] = true, ["Unarmed ships"] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, [5] = true, ["HelicopterCarrier"] = true, ["Side approach departure"] = true, [3] = true, ["All"] = true, [12] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [719] [720] = { ["type"] = "HarborTug", ["name"] = "Harbor Tug", ["category"] = "Ship", ["naval"] = true, ["description"] = "Harbor Tug", ["attribute"] = { [15] = true, ["Unarmed ships"] = true, [5] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, [3] = true, ["All"] = true, [12] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [720] [721] = { ["type"] = "Higgins_boat", ["name"] = "Boat LCVP Higgins", ["category"] = "Ship", ["naval"] = true, ["description"] = "Boat LCVP Higgins", ["attribute"] = { ["Light armed ships"] = true, [14] = true, ["NonArmoredUnits"] = true, ["Ships"] = true, ["Armed Ship"] = true, ["Armed ships"] = true, ["All"] = true, ["Naval"] = true, ["NonAndLightArmoredUnits"] = true, ["NO_SAM"] = true, [3] = true, [6] = true, ["low_reflection_vessel"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [721] [722] = { ["type"] = "hms_invincible", ["name"] = "HMS Invincible (R05)", ["category"] = "Ship", ["naval"] = true, ["description"] = "HMS Invincible (R05)", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier With Tramplin"] = true, ["AircraftCarrier"] = true, ["Ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, ["ski_jump"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, ["All"] = true, ["Armed ships"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Naval"] = true, [331] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [722] [723] = { ["type"] = "IMPROVED_KILO", ["name"] = "SSK 636 Improved Kilo", ["category"] = "Ship", ["naval"] = true, ["description"] = "SSK 636 Improved Kilo", ["attribute"] = { ["Submarines"] = true, ["Heavy armed ships"] = true, ["Ships"] = true, ["HeavyArmoredUnits"] = true, [16] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Naval"] = true, ["NO_SAM"] = true, [3] = true, ["All"] = true, [23] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [723] [724] = { ["type"] = "KILO", ["name"] = "SSK 877V Kilo", ["category"] = "Ship", ["naval"] = true, ["description"] = "SSK 877V Kilo", ["attribute"] = { ["Submarines"] = true, ["Heavy armed ships"] = true, ["Ships"] = true, ["HeavyArmoredUnits"] = true, [16] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Naval"] = true, ["NO_SAM"] = true, [3] = true, ["All"] = true, [23] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [724] [725] = { ["type"] = "KUZNECOW", ["name"] = "CV 1143.5 Admiral Kuznetsov", ["category"] = "Ship", ["naval"] = true, ["description"] = "CV 1143.5 Admiral Kuznetsov", ["attribute"] = { [1] = true, ["Aircraft Carriers"] = true, ["AircraftCarrier With Tramplin"] = true, ["AircraftCarrier"] = true, ["Ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["DetectionByAWACS"] = true, ["ski_jump"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, ["Arresting Gear"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Straight_in_approach_type"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, ["AircraftCarrier With Arresting Gear"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [725] [726] = { ["type"] = "La_Combattante_II", ["name"] = "FAC La Combattante IIa", ["category"] = "Ship", ["naval"] = true, ["description"] = "FAC La Combattante IIa", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, [14] = true, ["Armed Air Defence"] = true, ["Ships"] = true, ["Armed ships"] = true, [12] = true, ["Naval"] = true, [304] = true, ["Armed Ship"] = true, ["NO_SAM"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, ["Corvettes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [726] [727] = { ["type"] = "leander-gun-achilles", ["name"] = "HMS Achilles (F12)", ["category"] = "Ship", ["naval"] = true, ["description"] = "HMS Achilles (F12)", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, [14] = true, ["Armed Air Defence"] = true, ["Ships"] = true, ["Frigates"] = true, ["Armed ships"] = true, ["Naval"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [727] [728] = { ["type"] = "leander-gun-andromeda", ["name"] = "HMS Andromeda (F57)", ["category"] = "Ship", ["naval"] = true, ["description"] = "HMS Andromeda (F57)", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, [14] = true, ["Armed Air Defence"] = true, ["Ships"] = true, ["Frigates"] = true, ["Armed ships"] = true, ["Naval"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [728] [729] = { ["type"] = "leander-gun-ariadne", ["name"] = "HMS Ariadne (F72)", ["category"] = "Ship", ["naval"] = true, ["description"] = "HMS Ariadne (F72)", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, [14] = true, ["Armed Air Defence"] = true, ["Ships"] = true, ["Frigates"] = true, ["Armed ships"] = true, ["Naval"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [729] [730] = { ["type"] = "leander-gun-condell", ["name"] = "CNS Almirante Condell (PFG-06)", ["category"] = "Ship", ["naval"] = true, ["description"] = "CNS Almirante Condell (PFG-06)", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, [14] = true, ["Armed Air Defence"] = true, ["Ships"] = true, ["Frigates"] = true, ["Armed ships"] = true, ["Naval"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [730] [731] = { ["type"] = "leander-gun-lynch", ["name"] = "CNS Almirante Lynch (PFG-07)", ["category"] = "Ship", ["naval"] = true, ["description"] = "CNS Almirante Lynch (PFG-07)", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, [14] = true, ["Armed Air Defence"] = true, ["Ships"] = true, ["Frigates"] = true, ["Armed ships"] = true, ["Naval"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [731] [732] = { ["type"] = "LHA_Tarawa", ["name"] = "LHA-1 Tarawa", ["category"] = "Ship", ["naval"] = true, ["description"] = "LHA-1 Tarawa", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier With Tramplin"] = true, ["AircraftCarrier"] = true, ["Ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, ["ski_jump"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, ["Armed ships"] = true, ["Armed Air Defence"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, [269] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [732] [733] = { ["type"] = "LST_Mk2", ["name"] = "LST Mk.II", ["category"] = "Ship", ["naval"] = true, ["description"] = "LST Mk.II", ["attribute"] = { ["HeavyArmoredUnits"] = true, [14] = true, ["Armed Air Defence"] = true, ["Ships"] = true, ["Armed ships"] = true, ["Landing Ships"] = true, ["All"] = true, ["Naval"] = true, ["Armed Ship"] = true, ["NO_SAM"] = true, [3] = true, ["Heavy armed ships"] = true, [289] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [733] [734] = { ["type"] = "MOLNIYA", ["name"] = "Corvette 1241.1 Molniya", ["category"] = "Ship", ["naval"] = true, ["description"] = "Corvette 1241.1 Molniya", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, [14] = true, ["Armed Air Defence"] = true, [15] = true, ["Armed ships"] = true, ["Ships"] = true, [12] = true, ["Naval"] = true, ["Armed Ship"] = true, ["NO_SAM"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, ["Corvettes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [734] [735] = { ["type"] = "MOSCOW", ["name"] = "Cruiser 1164 Moskva", ["category"] = "Ship", ["naval"] = true, ["description"] = "Cruiser 1164 Moskva", ["attribute"] = { [13] = true, ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Ships"] = true, ["Armed ships"] = true, ["Naval"] = true, ["Cruisers"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [735] [736] = { ["type"] = "NEUSTRASH", ["name"] = "Frigate 11540 Neustrashimy", ["category"] = "Ship", ["naval"] = true, ["description"] = "Frigate 11540 Neustrashimy", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, [14] = true, [28] = true, ["Ships"] = true, ["Frigates"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, [12] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [736] [737] = { ["type"] = "PERRY", ["name"] = "FFG Oliver Hazard Perry", ["category"] = "Ship", ["naval"] = true, ["description"] = "FFG Oliver Hazard Perry", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, ["Armed Air Defence"] = true, [14] = true, ["Armed ships"] = true, ["Ships"] = true, ["Frigates"] = true, ["Naval"] = true, [17] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, [12] = true, ["DetectionByAWACS"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [737] [738] = { ["type"] = "PIOTR", ["name"] = "Battlecruiser 1144.2 Pyotr Velikiy", ["category"] = "Ship", ["naval"] = true, ["description"] = "Battlecruiser 1144.2 Pyotr Velikiy", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, ["Armed Air Defence"] = true, [14] = true, ["Armed ships"] = true, ["Ships"] = true, ["HelicopterCarrier"] = true, ["Naval"] = true, ["Cruisers"] = true, ["RADAR_BAND1_FOR_ARM"] = true, [19] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [738] [739] = { ["type"] = "REZKY", ["name"] = "Frigate 1135M Rezky", ["category"] = "Ship", ["naval"] = true, ["description"] = "Frigate 1135M Rezky", ["attribute"] = { [14] = true, ["Heavy armed ships"] = true, ["Ships"] = true, ["Frigates"] = true, ["HeavyArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Naval"] = true, ["Armed Ship"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["All"] = true, [12] = true, [3] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [739] [740] = { ["type"] = "santafe", ["name"] = "ARA Santa Fe S-21", ["category"] = "Ship", ["naval"] = true, ["description"] = "ARA Santa Fe S-21", ["attribute"] = { ["Submarines"] = true, ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, ["Armed Air Defence"] = true, [16] = true, ["Armed ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, [12] = true, [332] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [740] [741] = { ["type"] = "Schnellboot_type_S130", ["name"] = "Boat Schnellboot type S130", ["category"] = "Ship", ["naval"] = true, ["description"] = "Boat Schnellboot type S130", ["attribute"] = { ["Light armed ships"] = true, [292] = true, [14] = true, ["NonArmoredUnits"] = true, ["Ships"] = true, ["Armed Ship"] = true, ["Armed ships"] = true, ["NonAndLightArmoredUnits"] = true, ["NO_SAM"] = true, [3] = true, ["All"] = true, ["Naval"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [741] [742] = { ["type"] = "Seawise_Giant", ["name"] = "Tanker Seawise Giant", ["category"] = "Ship", ["naval"] = true, ["description"] = "Tanker Seawise Giant", ["attribute"] = { [15] = true, ["Unarmed ships"] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, ["All"] = true, ["HelicopterCarrier"] = true, ["Side approach departure"] = true, [3] = true, [303] = true, [12] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [742] [743] = { ["type"] = "Ship_Tilde_Supply", ["name"] = "Supply Ship MV Tilde", ["category"] = "Ship", ["naval"] = true, ["description"] = "Supply Ship MV Tilde", ["attribute"] = { [15] = true, ["Unarmed ships"] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, [5] = true, ["HelicopterCarrier"] = true, ["Side approach departure"] = true, [3] = true, ["All"] = true, [12] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [743] [744] = { ["type"] = "SOM", ["name"] = "SSK 641B Tango", ["category"] = "Ship", ["naval"] = true, ["description"] = "SSK 641B Tango", ["attribute"] = { [24] = true, ["Submarines"] = true, ["Ships"] = true, ["Heavy armed ships"] = true, [16] = true, ["HeavyArmoredUnits"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["NO_SAM"] = true, [3] = true, ["All"] = true, [12] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [744] [745] = { ["type"] = "speedboat", ["name"] = "Boat Armed Hi-speed", ["category"] = "Ship", ["naval"] = true, ["description"] = "Boat Armed Hi-speed", ["attribute"] = { ["Light armed ships"] = true, [14] = true, ["NonArmoredUnits"] = true, ["Ships"] = true, ["Armed Ship"] = true, ["Armed ships"] = true, ["All"] = true, ["Naval"] = true, ["NonAndLightArmoredUnits"] = true, ["NO_SAM"] = true, [3] = true, [6] = true, ["low_reflection_vessel"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [745] [746] = { ["type"] = "Stennis", ["name"] = "CVN-74 John C. Stennis", ["category"] = "Ship", ["naval"] = true, ["description"] = "CVN-74 John C. Stennis", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier"] = true, ["ACLS"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["AircraftCarrier With Arresting Gear"] = true, ["Link4"] = true, [3] = true, ["Heavy armed ships"] = true, [12] = true, [264] = true, ["AircraftCarrier With Catapult"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Ships"] = true, ["Arresting Gear"] = true, ["Naval"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["catapult"] = true, ["Datalink"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [746] [747] = { ["type"] = "TICONDEROG", ["name"] = "CG Ticonderoga", ["category"] = "Ship", ["naval"] = true, ["description"] = "CG Ticonderoga", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, ["Armed Air Defence"] = true, [14] = true, ["Armed ships"] = true, ["Ships"] = true, [21] = true, ["Naval"] = true, ["Cruisers"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [747] [748] = { ["type"] = "Type_052B", ["name"] = "Type 052B Destroyer", ["category"] = "Ship", ["naval"] = true, ["description"] = "Type 052B Destroyer", ["attribute"] = { [270] = true, ["Straight_in_approach_type"] = true, ["Ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["NO_SAM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, ["HelicopterCarrier"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Destroyers"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, [14] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [748] [749] = { ["type"] = "Type_052C", ["name"] = "Type 052C Destroyer", ["category"] = "Ship", ["naval"] = true, ["description"] = "Type 052C Destroyer", ["attribute"] = { ["Straight_in_approach_type"] = true, ["Ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, [272] = true, ["NO_SAM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, ["Armed ships"] = true, ["Cruisers"] = true, ["Armed Air Defence"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, [14] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [749] [750] = { ["type"] = "Type_054A", ["name"] = "Type 054A Frigate", ["category"] = "Ship", ["naval"] = true, ["description"] = "Type 054A Frigate", ["attribute"] = { ["Straight_in_approach_type"] = true, ["Ships"] = true, [271] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["NO_SAM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, [14] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, ["Frigates"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [750] [751] = { ["type"] = "Type_071", ["name"] = "Type 071 Amphibious Transport Dock", ["category"] = "Ship", ["naval"] = true, ["description"] = "Type 071 Amphibious Transport Dock", ["attribute"] = { ["Aircraft Carriers"] = true, ["AircraftCarrier With Tramplin"] = true, ["AircraftCarrier"] = true, ["Ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["NO_SAM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, [3] = true, [274] = true, [12] = true, ["Heavy armed ships"] = true, ["Armed ships"] = true, ["Armed Air Defence"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, ["Straight_in_approach_type"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [751] [752] = { ["type"] = "Type_093", ["name"] = "Type 093 Attack Submarine", ["category"] = "Ship", ["naval"] = true, ["description"] = "Type 093 Attack Submarine", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, ["Submarines"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Ships"] = true, ["All"] = true, [16] = true, ["Naval"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["Armed Ship"] = true, ["NO_SAM"] = true, [3] = true, [273] = true, [12] = true, ["DetectionByAWACS"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [752] [753] = { ["type"] = "Uboat_VIIC", ["name"] = "U-boat VIIC U-flak", ["category"] = "Ship", ["naval"] = true, ["description"] = "U-boat VIIC U-flak", ["attribute"] = { [291] = true, ["Submarines"] = true, ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, ["Armed Air Defence"] = true, [16] = true, ["Armed ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["Naval"] = true, ["NO_SAM"] = true, [3] = true, ["All"] = true, [12] = true, ["Armed Ship"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [753] [754] = { ["type"] = "USS_Arleigh_Burke_IIa", ["name"] = "DDG Arleigh Burke IIa", ["category"] = "Ship", ["naval"] = true, ["description"] = "DDG Arleigh Burke IIa", ["attribute"] = { ["Heavy armed ships"] = true, ["HeavyArmoredUnits"] = true, ["Armed Air Defence"] = true, [14] = true, ["Armed ships"] = true, ["Ships"] = true, [21] = true, ["Naval"] = true, ["Cruisers"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, [3] = true, ["All"] = true, ["DetectionByAWACS"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [754] [755] = { ["type"] = "USS_Samuel_Chase", ["name"] = "LS Samuel Chase", ["category"] = "Ship", ["naval"] = true, ["description"] = "LS Samuel Chase", ["attribute"] = { [290] = true, [14] = true, ["HeavyArmoredUnits"] = true, ["Ships"] = true, ["Armed Air Defence"] = true, ["Landing Ships"] = true, ["Armed ships"] = true, ["All"] = true, ["Naval"] = true, ["NO_SAM"] = true, [3] = true, ["Heavy armed ships"] = true, [12] = true, ["Armed Ship"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [755] [756] = { ["type"] = "VINSON", ["name"] = "CVN-70 Carl Vinson", ["category"] = "Ship", ["naval"] = true, ["description"] = "CVN-70 Carl Vinson", ["attribute"] = { [2] = true, ["Aircraft Carriers"] = true, ["AircraftCarrier"] = true, ["Arresting Gear"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["AircraftCarrier With Arresting Gear"] = true, [3] = true, [12] = true, ["AircraftCarrier With Catapult"] = true, ["Heavy armed ships"] = true, ["Ships"] = true, ["Armed Air Defence"] = true, ["Armed ships"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["catapult"] = true, ["Naval"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [756] [757] = { ["type"] = "ZWEZDNY", ["name"] = "Boat Zvezdny type", ["category"] = "Ship", ["naval"] = true, ["description"] = "Boat Zvezdny type", ["attribute"] = { [15] = true, ["Unarmed ships"] = true, ["HeavyArmoredUnits"] = true, [5] = true, ["Ships"] = true, ["Naval"] = true, [3] = true, ["All"] = true, ["low_reflection_vessel"] = true, [12] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [757] [758] = { ["type"] = "AA8", ["name"] = "Firefighter Vehicle AA-7.2/60", ["category"] = "Unarmed", ["description"] = "Firefighter Vehicle AA-7.2/60", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, [295] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [758] [759] = { ["type"] = "ATMZ-5", ["name"] = "Refueler ATMZ-5", ["category"] = "Unarmed", ["description"] = "Refueler ATMZ-5", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, [4] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [759] [760] = { ["type"] = "ATZ-10", ["name"] = "Refueler ATZ-10", ["category"] = "Unarmed", ["description"] = "Refueler ATZ-10", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, [5] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "ATZ-10 Fuel Truck", }, -- end of ["aliases"] }, -- end of [760] [761] = { ["type"] = "ATZ-5", ["name"] = "Refueler ATZ-5", ["category"] = "Unarmed", ["description"] = "Refueler ATZ-5", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [294] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [761] [762] = { ["type"] = "ATZ-60_Maz", ["name"] = "Refueler ATZ-60 Tractor (MAZ-7410)", ["category"] = "Unarmed", ["description"] = "Refueler ATZ-60 Tractor (MAZ-7410)", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, [310] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [762] [763] = { ["type"] = "B600_drivable", ["name"] = "M92 B600 drivable", ["category"] = "Unarmed", ["description"] = "M92 B600 drivable", ["vehicle"] = true, ["attribute"] = { [25] = true, ["human_vehicle"] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [17] = true, ["Ground Units Non Airdefence"] = true, ["Cars"] = true, [38] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [763] [764] = { ["type"] = "Bedford_MWD", ["name"] = "Truck Bedford", ["category"] = "Unarmed", ["description"] = "Truck Bedford", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [764] [765] = { ["type"] = "Blitz_36-6700A", ["name"] = "Truck Opel Blitz", ["category"] = "Unarmed", ["description"] = "Truck Opel Blitz", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [765] [766] = { ["type"] = "CCKW_353", ["name"] = "Truck GMC \"Jimmy\" 6x6", ["category"] = "Unarmed", ["description"] = "Truck GMC \"Jimmy\" 6x6", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [766] [767] = { ["type"] = "CHAP_M1083", ["name"] = "Truck M1083 A1P2 MTV [CH]", ["category"] = "Unarmed", ["description"] = "Truck M1083 A1P2 MTV [CH]", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [353] = true, [2] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [767] [768] = { ["type"] = "GAZ-3307", ["name"] = "Truck GAZ-3307", ["category"] = "Unarmed", ["description"] = "Truck GAZ-3307", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, [68] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [768] [769] = { ["type"] = "GAZ-3308", ["name"] = "Truck GAZ-3308", ["category"] = "Unarmed", ["description"] = "Truck GAZ-3308", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, [69] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [769] [770] = { ["type"] = "GAZ-66", ["name"] = "Truck GAZ-66", ["category"] = "Unarmed", ["description"] = "Truck GAZ-66", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, [67] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [770] [771] = { ["type"] = "gaz-66_civil", ["name"] = "Truck GAZ-66 civil", ["category"] = "Unarmed", ["description"] = "Truck GAZ-66 civil", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, [67] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [771] [772] = { ["type"] = "GD-20", ["name"] = "GD-20 Lift Truck", ["category"] = "Unarmed", ["description"] = "GD-20 Lift Truck", ["vehicle"] = true, ["attribute"] = { [25] = true, ["human_vehicle"] = true, ["Unarmed vehicles"] = true, [2] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Trucks"] = true, [17] = true, ["Ground Units Non Airdefence"] = true, ["Cars"] = true, ["Ground vehicles"] = true, ["Vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [378] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [772] [773] = { ["type"] = "GPS_Spoofer_Blue", ["name"] = "GPS Spoofer NATO", ["category"] = "Unarmed", ["description"] = "GPS Spoofer NATO", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["Trucks"] = true, [17] = true, ["NonAndLightArmoredUnits"] = true, ["Jammer"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [379] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [773] [774] = { ["type"] = "GPS_Spoofer_Red", ["name"] = "GPS Spoofer RF", ["category"] = "Unarmed", ["description"] = "GPS Spoofer RF", ["vehicle"] = true, ["attribute"] = { [25] = true, [382] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["Trucks"] = true, [17] = true, ["NonAndLightArmoredUnits"] = true, ["Jammer"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [774] [775] = { ["type"] = "HEMTT TFFT", ["name"] = "Firefighter HEMMT TFFT", ["category"] = "Unarmed", ["description"] = "Firefighter HEMMT TFFT", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [775] [776] = { ["type"] = "Horch_901_typ_40_kfz_21", ["name"] = "LUV Horch 901 Staff Car", ["category"] = "Unarmed", ["description"] = "LUV Horch 901 Staff Car", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [776] [777] = { ["type"] = "Hummer", ["name"] = "LUV HMMWV Jeep", ["category"] = "Unarmed", ["description"] = "LUV HMMWV Jeep", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["human_vehicle"] = true, ["Infantry carriers"] = true, [25] = true, ["LightArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [14] = true, ["Armed vehicles"] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "M1025 HMMWV", }, -- end of ["aliases"] }, -- end of [777] [778] = { ["type"] = "IKARUS Bus", ["name"] = "Bus IKARUS-280", ["category"] = "Unarmed", ["description"] = "Bus IKARUS-280", ["vehicle"] = true, ["attribute"] = { [46] = true, [25] = true, ["Vehicles"] = true, [2] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [778] [779] = { ["type"] = "KAMAZ Truck", ["name"] = "Truck KAMAZ 43101", ["category"] = "Unarmed", ["description"] = "Truck KAMAZ 43101", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [57] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "KAMAZ-43101", }, -- end of ["aliases"] }, -- end of [779] [780] = { ["type"] = "kamaz_tent_civil", ["name"] = "Truck KAMAZ-43101 civil", ["category"] = "Unarmed", ["description"] = "Truck KAMAZ-43101 civil", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [57] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "KAMAZ-43101", }, -- end of ["aliases"] }, -- end of [780] [781] = { ["type"] = "KrAZ6322", ["name"] = "Truck KrAZ-6322 6x6", ["category"] = "Unarmed", ["description"] = "Truck KrAZ-6322 6x6", ["vehicle"] = true, ["attribute"] = { [25] = true, ["human_vehicle"] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["Trucks"] = true, [17] = true, ["Ground Units Non Airdefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, ["Vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [781] [782] = { ["type"] = "Kubelwagen_82", ["name"] = "LUV Kubelwagen Jeep", ["category"] = "Unarmed", ["description"] = "LUV Kubelwagen Jeep", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [782] [783] = { ["type"] = "Land_Rover_101_FC", ["name"] = "Truck Land Rover 101 FC", ["category"] = "Unarmed", ["description"] = "Truck Land Rover 101 FC", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [783] [784] = { ["type"] = "Land_Rover_109_S3", ["name"] = "LUV Land Rover 109", ["category"] = "Unarmed", ["description"] = "LUV Land Rover 109", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [784] [785] = { ["type"] = "LARC-V", ["name"] = "Truck LARC-V", ["category"] = "Unarmed", ["description"] = "Truck LARC-V", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [333] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [785] [786] = { ["type"] = "LAZ Bus", ["name"] = "Bus LAZ-695", ["category"] = "Unarmed", ["description"] = "Bus LAZ-695", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, [58] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [786] [787] = { ["type"] = "LiAZ Bus", ["name"] = "Bus LiAZ-677", ["category"] = "Unarmed", ["description"] = "Bus LiAZ-677", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, [58] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [787] [788] = { ["type"] = "M 818", ["name"] = "Truck M939 Heavy", ["category"] = "Unarmed", ["description"] = "Truck M939 Heavy", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["Trucks"] = true, [17] = true, ["Ground Units Non Airdefence"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground vehicles"] = true, [6] = true, ["All"] = true, ["Datalink"] = true, ["Ground Units"] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "M818", }, -- end of ["aliases"] }, -- end of [788] [789] = { ["type"] = "M30_CC", ["name"] = "Ammo M30 Cargo Carrier", ["category"] = "Unarmed", ["description"] = "Ammo M30 Cargo Carrier", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [789] [790] = { ["type"] = "M978 HEMTT Tanker", ["name"] = "Refueler M978 HEMTT", ["category"] = "Unarmed", ["description"] = "Refueler M978 HEMTT", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [790] [791] = { ["type"] = "MAZ-6303", ["name"] = "Truck MAZ-6303", ["category"] = "Unarmed", ["description"] = "Truck MAZ-6303", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [70] = true, [2] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [791] [792] = { ["type"] = "MJ-1_drivable", ["name"] = "M92 MJ-1 drivable", ["category"] = "Unarmed", ["description"] = "M92 MJ-1 drivable", ["vehicle"] = true, ["attribute"] = { [25] = true, ["human_vehicle"] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [17] = true, ["Ground Units Non Airdefence"] = true, ["Cars"] = true, [38] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [792] [793] = { ["type"] = "P20_drivable", ["name"] = "M92 P20 drivable", ["category"] = "Unarmed", ["description"] = "M92 P20 drivable", ["vehicle"] = true, ["attribute"] = { [25] = true, ["human_vehicle"] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [17] = true, ["Ground Units Non Airdefence"] = true, ["Cars"] = true, [38] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [793] [794] = { ["type"] = "Predator GCS", ["name"] = "MCC Predator UAV CP & GCS", ["category"] = "Unarmed", ["description"] = "MCC Predator UAV CP & GCS", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [794] [795] = { ["type"] = "Predator TrojanSpirit", ["name"] = "MCC-COMM Predator UAV CL", ["category"] = "Unarmed", ["description"] = "MCC-COMM Predator UAV CL", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [795] [796] = { ["type"] = "prmg_gp_beacon", ["name"] = "PRMG Glidepath car", ["category"] = "Unarmed", ["description"] = "PRMG Glidepath car", ["attribute"] = { [17] = true, [25] = true, ["PRMG_GLIDEPATH"] = true, [384] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [796] [797] = { ["type"] = "prmg_loc_beacon", ["name"] = "PRMG Localizer car", ["category"] = "Unarmed", ["description"] = "PRMG Localizer car", ["attribute"] = { [17] = true, [25] = true, [385] = true, ["PRMG_LOCALIZER"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [797] [798] = { ["type"] = "r11_volvo_drivable", ["name"] = "M92 R11 Volvo drivable", ["category"] = "Unarmed", ["description"] = "M92 R11 Volvo drivable", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [798] [799] = { ["type"] = "rsbn_beacon", ["name"] = "RSBN car", ["category"] = "Unarmed", ["description"] = "RSBN car", ["attribute"] = { [17] = true, [383] = true, [25] = true, ["RSBN"] = true, [2] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [799] [800] = { ["type"] = "S_75_ZIL", ["name"] = "S-75 Tractor (ZIL-131)", ["category"] = "Unarmed", ["description"] = "S-75 Tractor (ZIL-131)", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [338] = true, [2] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [800] [801] = { ["type"] = "Sd_Kfz_2", ["name"] = "LUV Kettenrad", ["category"] = "Unarmed", ["description"] = "LUV Kettenrad", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [801] [802] = { ["type"] = "Sd_Kfz_7", ["name"] = "Tractor Sd.Kfz.7 Art'y Tractor", ["category"] = "Unarmed", ["description"] = "Tractor Sd.Kfz.7 Art'y Tractor", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [26] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [802] [803] = { ["type"] = "SKP-11", ["name"] = "Truck SKP-11 Mobile ATC", ["category"] = "Unarmed", ["description"] = "Truck SKP-11 Mobile ATC", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "SKP-11 Mobile Command Post", }, -- end of ["aliases"] }, -- end of [803] [804] = { ["type"] = "Suidae", ["name"] = "Suidae", ["category"] = "Unarmed", ["description"] = "Suidae", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, [47] = true, ["NonAndLightArmoredUnits"] = true, ["Cars"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [804] [805] = { ["type"] = "tacr2a", ["name"] = "Firefighter RAF Rescue", ["category"] = "Unarmed", ["description"] = "Firefighter RAF Rescue", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, [340] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, ["Ground vehicles"] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [25] = true, ["NonAndLightArmoredUnits"] = true, [14] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [805] [806] = { ["type"] = "Tigr_233036", ["name"] = "LUV Tigr", ["category"] = "Unarmed", ["description"] = "LUV Tigr", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["human_vehicle"] = true, ["Infantry carriers"] = true, [25] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [806] [807] = { ["type"] = "Trolley bus", ["name"] = "ZIU-9 Trolley", ["category"] = "Unarmed", ["description"] = "ZIU-9 Trolley", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, [49] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [807] [808] = { ["type"] = "TugHarlan_drivable", ["name"] = "M92 Tug Harlan drivable", ["category"] = "Unarmed", ["description"] = "M92 Tug Harlan drivable", ["vehicle"] = true, ["attribute"] = { [25] = true, ["human_vehicle"] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [17] = true, ["Ground Units Non Airdefence"] = true, ["Cars"] = true, [38] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [808] [809] = { ["type"] = "Type_94_Truck", ["name"] = "Truck Type 94", ["category"] = "Unarmed", ["description"] = "Truck Type 94", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [809] [810] = { ["type"] = "Type_98_So_Da", ["name"] = "APC Type 98 So Da", ["category"] = "Unarmed", ["description"] = "APC Type 98 So Da", ["vehicle"] = true, ["attribute"] = { [2] = true, ["Vehicles"] = true, ["Armored vehicles"] = true, ["AntiAir Armed Vehicles"] = true, [17] = true, ["Ground vehicles"] = true, [10] = true, ["Armed ground units"] = true, ["APC"] = true, ["Ground Units Non Airdefence"] = true, ["Infantry carriers"] = true, [25] = true, ["NonAndLightArmoredUnits"] = true, ["LightArmoredUnits"] = true, ["All"] = true, ["Ground Units"] = true, ["Armed vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [810] [811] = { ["type"] = "TZ-22_KrAZ", ["name"] = "Refueler TZ-22 Tractor (KrAZ-258B1)", ["category"] = "Unarmed", ["description"] = "Refueler TZ-22 Tractor (KrAZ-258B1)", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [312] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [811] [812] = { ["type"] = "UAZ-469", ["name"] = "LUV UAZ-469 Jeep", ["category"] = "Unarmed", ["description"] = "LUV UAZ-469 Jeep", ["vehicle"] = true, ["attribute"] = { [25] = true, ["human_vehicle"] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, [17] = true, ["Ground Units Non Airdefence"] = true, ["Cars"] = true, [38] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, ["Vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [812] [813] = { ["type"] = "Ural ATsP-6", ["name"] = "Firefighter Ural ATsP-6", ["category"] = "Unarmed", ["description"] = "Firefighter Ural ATsP-6", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [813] [814] = { ["type"] = "Ural-375", ["name"] = "Truck Ural-4320", ["category"] = "Unarmed", ["description"] = "Truck Ural-4320", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, [40] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [814] [815] = { ["type"] = "Ural-375 PBU", ["name"] = "Truck Ural-4320 MCC", ["category"] = "Unarmed", ["description"] = "Truck Ural-4320 MCC", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, [41] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [815] [816] = { ["type"] = "Ural-4320 APA-5D", ["name"] = "GPU APA-5D on Ural 4320", ["category"] = "Unarmed", ["description"] = "GPU APA-5D on Ural 4320", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "Ural-4320 APA-5D Ground Power Unit", }, -- end of ["aliases"] }, -- end of [816] [817] = { ["type"] = "Ural-4320-31", ["name"] = "Truck Ural-4320-31 Arm'd", ["category"] = "Unarmed", ["description"] = "Truck Ural-4320-31 Arm'd", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [817] [818] = { ["type"] = "Ural-4320T", ["name"] = "Truck Ural-4320T", ["category"] = "Unarmed", ["description"] = "Truck Ural-4320T", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [75] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [818] [819] = { ["type"] = "ural_4230_civil_b", ["name"] = "Truck Ural-4320 civil", ["category"] = "Unarmed", ["description"] = "Truck Ural-4320 civil", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, [40] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [819] [820] = { ["type"] = "ural_4230_civil_t", ["name"] = "Truck Ural-4320T civil", ["category"] = "Unarmed", ["description"] = "Truck Ural-4320T civil", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [75] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [820] [821] = { ["type"] = "ural_atz5_civil", ["name"] = "Refueler ATZ-5 civil", ["category"] = "Unarmed", ["description"] = "Refueler ATZ-5 civil", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, [386] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [821] [822] = { ["type"] = "VAZ Car", ["name"] = "Car VAZ-2109", ["category"] = "Unarmed", ["description"] = "Car VAZ-2109", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["NonArmoredUnits"] = true, [47] = true, ["NonAndLightArmoredUnits"] = true, ["Cars"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [17] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [822] [823] = { ["type"] = "Willys_MB", ["name"] = "Car Willys Jeep", ["category"] = "Unarmed", ["description"] = "Car Willys Jeep", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [823] [824] = { ["type"] = "ZiL-131 APA-80", ["name"] = "GPU APA-80 on ZIL-131", ["category"] = "Unarmed", ["description"] = "GPU APA-80 on ZIL-131", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [6] = true, }, -- end of ["attribute"] ["aliases"] = { [1] = "ZiL-131 APA-80 Ground Power Unit", }, -- end of ["aliases"] }, -- end of [824] [825] = { ["type"] = "ZIL-131 KUNG", ["name"] = "Truck ZIL-131 (C2)", ["category"] = "Unarmed", ["description"] = "Truck ZIL-131 (C2)", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, [79] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [825] [826] = { ["type"] = "zil-131_civil", ["name"] = "Truck ZiL-131 civil", ["category"] = "Unarmed", ["description"] = "Truck ZiL-131 civil", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [387] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [826] [827] = { ["type"] = "ZIL-135", ["name"] = "Truck ZIL-135", ["category"] = "Unarmed", ["description"] = "Truck ZIL-135", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, [311] = true, ["Trucks"] = true, [17] = true, ["Unarmed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["NonArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["All"] = true, ["Ground Units"] = true, ["Ground vehicles"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [827] [828] = { ["type"] = "ZIL-4331", ["name"] = "Truck ZIL-4331", ["category"] = "Unarmed", ["description"] = "Truck ZIL-4331", ["vehicle"] = true, ["attribute"] = { [25] = true, ["Vehicles"] = true, [2] = true, ["Unarmed vehicles"] = true, ["Trucks"] = true, [17] = true, ["NonArmoredUnits"] = true, ["NonAndLightArmoredUnits"] = true, ["Ground Units Non Airdefence"] = true, ["Ground vehicles"] = true, ["All"] = true, ["Ground Units"] = true, [71] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [828] [829] = { ["type"] = ".Ammunition depot", ["name"] = "Ammunition depot", ["category"] = "Warehouse", ["description"] = "Ammunition depot", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [829] [830] = { ["type"] = "Tank", ["name"] = "Tank 1", ["category"] = "Warehouse", ["description"] = "Tank 1", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [830] [831] = { ["type"] = "Tank 2", ["name"] = "Tank 2", ["category"] = "Warehouse", ["description"] = "Tank 2", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [831] [832] = { ["type"] = "Tank 3", ["name"] = "Tank 3", ["category"] = "Warehouse", ["description"] = "Tank 3", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [832] [833] = { ["type"] = "Warehouse", ["name"] = "Warehouse", ["category"] = "Warehouse", ["description"] = "Warehouse", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [833] } -- end of units ------------------ END script dcsUnits.lua ------------------ ------------------ START script veafCacheManager.lua ------------------ ------------------------------------------------------------------ -- VEAF Cache Manager -- By Zip (2024-25) -- -- Features: -- --------- -- * Manage cached data with fixed or flexible lifespan ------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafCacheManager = {} --- Identifier. All output in the log will start with this. veafCacheManager.Id = "CACHE - " --- Version. veafCacheManager.Version = "0.0.1" -- trace level, specific to this module veafCacheManager.LogLevel = "trace" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veaf.loggers.new(veafCacheManager.Id, veafCacheManager.LogLevel) ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafCache class ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafCache = {} VeafCache.DEFAULT_TIME_TO_LIVE = 1 -- 1 second VeafCache.LIVE_FOREVER = -1 -- this is the TTL for a computed once, cached forever data ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- CTOR function VeafCache.init(object) -- technical name (VeafCache instance name) object.name = nil -- cache data object.cache = {} -- default time to live object.defaultTTL = VeafCache.DEFAULT_TIME_TO_LIVE end function VeafCache:new(objectToCopy) veaf.loggers.get(veafCacheManager.Id):debug("VeafCache:new()") local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object VeafCache.init(objectToCreate) return objectToCreate end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- PROPERTIES -- technical name (VeafCache instance name) function VeafCache:setName(value) veaf.loggers.get(veafCacheManager.Id):debug("VeafCache[]:setName(%s)", veaf.p(value)) self.name = value end -- technical name (VeafCache instance name) function VeafCache:getName() return self.name or self.description end -- default time to live function VeafCache:setDefaultTimeToLive(value) self.defaultTTL = value veaf.loggers.get(veafCacheManager.Id):debug("VeafCache[%s]:setDefaultTimeToLive(%s)", veaf.p(self:getName()), veaf.p(value)) return self end -- default time to live function VeafCache:getDefaultTimeToLive() return self.defaultTTL end -- remove cached data function VeafCache:delCachedData(key) if self.cache then self.cache[key] = nil end end -- set cached data function VeafCache:setCachedData(key, value, timetolive) local cachedData = nil if self.cache then local _endoflife = timer.getTime() + (timetolive or self:getDefaultTimeToLive()) if timetolive == VeafCache.LIVE_FOREVER then _endoflife = VeafCache.LIVE_FOREVER end cachedData = { data = value, endoflife = _endoflife } self.cache[key] = cachedData end return cachedData end -- get cached data function VeafCache:getCachedData(key) if self.cache then local cachedData = self.cache[key] if cachedData and cachedData.endoflife < timer.getTime() then return cachedData end end return nil end ------------------ END script veafCacheManager.lua ------------------ ------------------ START script veafEventHandler.lua ------------------ ------------------------------------------------------------------ -- VEAF Event handler for DCS World -- By Zip (2023) -- -- Features: -- --------- -- * handles DCS events -- * can be plugged to objects that react to events -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafEventHandler = {} --- Identifier. All output in the log will start with this. veafEventHandler.Id = "EVENTS - " --- Version. veafEventHandler.Version = "1.5.2" -- trace level, specific to this module --veafEventHandler.LogLevel = "trace" veafEventHandler.CALLBACK_DELAY = 0.5 -- seconds ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veaf.loggers.new(veafEventHandler.Id, veafEventHandler.LogLevel) function veafEventHandler.completeUnit(unit) if unit ~= nil and unit.getName then local unitName = unit:getName() return veafEventHandler.completeUnitFromName(unitName) else return nil end end function veafEventHandler.completeUnitFromName(unitName) veaf.loggers.get(veafEventHandler.Id):trace("veafEventHandler.completeUnitFromName(unitName=%s)", veaf.p(unitName)) if unitName ~= nil then local unitType = nil local unitLifePercent = nil local unitLife = 0 local unitCoalition = coalition.side.NEUTRAL local unitCategory = nil local unitGroupName = nil local unitGroupId = nil local unit = Unit.getByName(unitName) local unitCallsign = nil if unit then if unit.getTypeName then unitType = unit:getTypeName() end if unit.getLife then unitLife = unit:getLife() end if unit.getCoalition then unitCoalition = unit:getCoalition() end local unitLife0 = 0 if unit.getLife0 then -- statics have no life0 unitLife0 = unit:getLife0() end unitLifePercent = unitLife if unitLife0 > 0 then unitLifePercent = 100 * unitLife / unitLife0 end if unit.getCategory then unitCategory = unit:getCategory() end if unit.getGroup then local group = unit:getGroup() if group then unitGroupName = group:getName() unitGroupId = group:getID() end end if unit.getCallsign then unitCallsign = unit:getCallsign() if type(unitCallsign) == "table" then unitCallsign = unitCallsign["name"] end if type(unitCallsign) == "number" then unitCallsign = "" .. unitCallsign end end end local unitPilotName = nil local unitPilotUcid = nil local unitPilot = veafRemote.getRemoteUserFromUnit(unitName) if unitPilot then veaf.loggers.get(veafEventHandler.Id):trace("unitPilot=%s", veaf.p(unitPilot)) unitPilotName = unitPilot.name unitPilotUcid = unitPilot.ucid end if unitPilotName then unitCallsign = unitPilotName end return { unitName = unitName, unitCallsign = unitCallsign, unitType = unitType, unitGroupName = unitGroupName, unitGroupId = unitGroupId, unitCoalition = unitCoalition, unitCategory = unitCategory, unitPilotName = unitPilotName, unitPilotUcid = unitPilotUcid, unitLifePercent = unitLifePercent } else return nil end end --- name self explanatory --- events the list of event names or ids that your callback is interested into --- callback the function to be called (we'll pass it an event; the definition of "event" can be found just below) function veafEventHandler.addCallback(name, events, callback) veaf.loggers.get(veafEventHandler.Id):debug("veafEventHandler.addCallback(name=[%s])", name) veaf.loggers.get(veafEventHandler.Id):trace("veafEventHandler.addCallback(events=[%s])", events) if name == nil then veaf.loggers.get(veafEventHandler.Id):error("veafEventHandler.addCallback: parameter `name` is mandatory") return false end if callback == nil then veaf.loggers.get(veafEventHandler.Id):error("veafEventHandler.addCallback: parameter `callback` is mandatory") return false end if events ~= nil then -- validate all event types for _, eventNameOrId in pairs(events) do if not(veafEventHandler.checkEventKnown(eventNameOrId)) then return false end end end table.insert(veafEventHandler.callbacks, {name=name, events=events, call=callback}) return true end veafEventHandler.callbacks = {} local function transformEvent(event) local _event = { type = veafEventHandler.knownEvents[event.id], time = event.time, idx = event.idx, coordinates = event.pos, text = event.text, coalition = event.coalition, groupId = event.groupID, place = veafEventHandler.completeUnit(event.place), birthPlace = event.subPlace, initiator = veafEventHandler.completeUnit(event.initiator), target = veafEventHandler.completeUnit(event.target), weapon = event.weapon, weaponName = event.weapon_name, comment = event.comment } return _event end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Main event handler (used for PLAYER ENTER UNIT events) ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Event handler. veafEventHandler.eventHandler = {} veafEventHandler.EVENTS = { [0] = { name = "S_EVENT_INVALID", id = 0, enabled = false }, [1] = { name = "S_EVENT_SHOT", id = 1, enabled = true }, [2] = { name = "S_EVENT_HIT", id = 2, enabled = true }, [3] = { name = "S_EVENT_TAKEOFF", id = 3, enabled = true }, [4] = { name = "S_EVENT_LAND", id = 4, enabled = true }, [5] = { name = "S_EVENT_CRASH", id = 5, enabled = true }, [6] = { name = "S_EVENT_EJECTION", id = 6, enabled = true }, [7] = { name = "S_EVENT_REFUELING", id = 7, enabled = true }, [8] = { name = "S_EVENT_DEAD", id = 8, enabled = true }, [9] = { name = "S_EVENT_PILOT_DEAD", id = 9, enabled = true }, [10] = { name = "S_EVENT_BASE_CAPTURED", id = 10, enabled = true }, [11] = { name = "S_EVENT_MISSION_START", id = 11, enabled = true }, [12] = { name = "S_EVENT_MISSION_END", id = 12, enabled = true }, [13] = { name = "S_EVENT_TOOK_CONTROL", id = 13, enabled = true }, [14] = { name = "S_EVENT_REFUELING_STOP", id = 14, enabled = true }, [15] = { name = "S_EVENT_BIRTH", id = 15, enabled = true, delaycallback = true }, [16] = { name = "S_EVENT_HUMAN_FAILURE", id = 16, enabled = true }, [17] = { name = "S_EVENT_DETAILED_FAILURE", id = 17, enabled = true }, [18] = { name = "S_EVENT_ENGINE_STARTUP", id = 18, enabled = true }, [19] = { name = "S_EVENT_ENGINE_SHUTDOWN", id = 19, enabled = true }, [20] = { name = "S_EVENT_PLAYER_ENTER_UNIT", id = 20, enabled = true, delaycallback = true }, [21] = { name = "S_EVENT_PLAYER_LEAVE_UNIT", id = 21, enabled = true }, [22] = { name = "S_EVENT_PLAYER_COMMENT", id = 22, enabled = true }, [23] = { name = "S_EVENT_SHOOTING_START", id = 23, enabled = true }, [24] = { name = "S_EVENT_SHOOTING_END", id = 24, enabled = true }, [25] = { name = "S_EVENT_MARK_ADDED", id = 25, enabled = true }, [26] = { name = "S_EVENT_MARK_CHANGE", id = 26, enabled = true }, [27] = { name = "S_EVENT_MARK_REMOVED", id = 27, enabled = true }, [28] = { name = "S_EVENT_KILL", id = 28, enabled = true }, [29] = { name = "S_EVENT_SCORE", id = 29, enabled = true }, [30] = { name = "S_EVENT_UNIT_LOST", id = 30, enabled = true }, [31] = { name = "S_EVENT_LANDING_AFTER_EJECTION", id = 31, enabled = true }, [32] = { name = "S_EVENT_PARATROOPER_LENDING", id = 32, enabled = true }, [33] = { name = "S_EVENT_DISCARD_CHAIR_AFTER_EJECTION", id = 33, enabled = true }, [34] = { name = "S_EVENT_WEAPON_ADD", id = 34, enabled = true }, [35] = { name = "S_EVENT_TRIGGER_ZONE", id = 35, enabled = true }, [36] = { name = "S_EVENT_LANDING_QUALITY_MARK", id = 36, enabled = true }, [37] = { name = "S_EVENT_BDA", id = 37, enabled = true }, [38] = { name = "S_EVENT_AI_ABORT_MISSION", id = 38, enabled = true }, [39] = { name = "S_EVENT_DAYNIGHT", id = 39, enabled = true }, [40] = { name = "S_EVENT_FLIGHT_TIME", id = 40, enabled = true }, [41] = { name = "S_EVENT_PLAYER_SELF_KILL_PILOT", id = 41, enabled = true }, [42] = { name = "S_EVENT_PLAYER_CAPTURE_AIRFIELD", id = 42, enabled = true }, [43] = { name = "S_EVENT_EMERGENCY_LANDING", id = 43, enabled = true }, [44] = { name = "S_EVENT_UNIT_CREATE_TASK", id = 44, enabled = true }, [45] = { name = "S_EVENT_UNIT_DELETE_TASK", id = 45, enabled = true }, [46] = { name = "S_EVENT_SIMULATION_START", id = 46, enabled = true }, [47] = { name = "S_EVENT_WEAPON_REARM", id = 47, enabled = true }, [48] = { name = "S_EVENT_WEAPON_DROP", id = 48, enabled = true }, [49] = { name = "S_EVENT_UNIT_TASK_COMPLETE", id = 49, enabled = true }, [50] = { name = "S_EVENT_UNIT_TASK_STAGE", id = 50, enabled = true }, [51] = { name = "S_EVENT_MAC_EXTRA_SCORE", id = 51, enabled = true }, [52] = { name = "S_EVENT_MISSION_RESTART", id = 52, enabled = true }, [53] = { name = "S_EVENT_MISSION_WINNER", id = 53, enabled = true }, [54] = { name = "S_EVENT_RUNWAY_TAKEOFF", id = 54, enabled = true }, [55] = { name = "S_EVENT_RUNWAY_TOUCH", id = 55, enabled = true }, [56] = { name = "S_EVENT_MAC_LMS_RESTART", id = 56, enabled = true }, [57] = { name = "S_EVENT_SIMULATION_FREEZE", id = 57, enabled = true }, [58] = { name = "S_EVENT_SIMULATION_UNFREEZE", id = 58, enabled = true }, [59] = { name = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_START", id = 59, enabled = true }, [60] = { name = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH", id = 60, enabled = true }, [61] = { name = "S_EVENT_MAX", id = 61, enabled = false } } veafEventHandler.unknownEvents = {} -- will be used to remember already signaled unknown events function veafEventHandler.checkEventKnown(eventNameOrId, warnOnly) veaf.loggers.get(veafEventHandler.Id):trace("veafEventHandler.checkEventKnown(eventNameOrId=%s)", eventNameOrId) if veafEventHandler.knownEvents[eventNameOrId] ~= nil then return true elseif veafEventHandler.unknownEvents[eventNameOrId] == nil then veafEventHandler.unknownEvents[eventNameOrId] = true local message = string.format("Event is not recognized by veafEventHandler: [%s]", veaf.p(eventNameOrId)) if warnOnly then veaf.loggers.get(veafEventHandler.Id):warn(message) else veaf.loggers.get(veafEventHandler.Id):error(message) end end return false end function veafEventHandler.setEventEnabled(eventNameOrId, enabled) if veafEventHandler.checkEventKnown(eventNameOrId) then veafEventHandler.knownEvents[eventNameOrId].enabled = enabled end end function veafEventHandler.isEventEnabled(eventNameOrId) if veafEventHandler.checkEventKnown(eventNameOrId) then return veafEventHandler.knownEvents[eventNameOrId].enabled end end function veafEventHandler.isEventDelayedCallback(eventNameOrId) if veafEventHandler.checkEventKnown(eventNameOrId) then return veafEventHandler.knownEvents[eventNameOrId].delaycallback end end --- Handle world events. function veafEventHandler.eventHandler:onEvent(event) if event == nil then veaf.loggers.get(veafEventHandler.Id):error("Event handler was called with a nil event!") return end -- check that we know the event if not(veafEventHandler.checkEventKnown(event.id, true)) then return end -- skip disabled events if not(veafEventHandler.isEventEnabled(event.id)) then return true end local _event = transformEvent(event) -- Debug output. if veaf.loggers.get(veafEventHandler.Id):wouldLogTrace() then veaf.loggers.get(veafEventHandler.Id):trace("event = %s", veaf.p(event)) veaf.loggers.get(veafEventHandler.Id):trace("_event = %s", veaf.p(_event)) end -- process event for _, callback in pairs(veafEventHandler.callbacks) do local callIt = false if callback.events == nil then callIt = true else for _, eventNameOrId in pairs(callback.events) do if _event.type.id == eventNameOrId or _event.type.name == eventNameOrId then callIt = true break end end end if callIt then if veafEventHandler.isEventDelayedCallback(_event.type.id) then veaf.loggers.get(veafEventHandler.Id):debug("delayed callback %s", veaf.p(callback.name)) timer.scheduleFunction(callback.call, _event, timer.getTime() + veafEventHandler.CALLBACK_DELAY) else veaf.loggers.get(veafEventHandler.Id):debug("calling callback %s", veaf.p(callback.name)) callback.call(_event) end end end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Other functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafEventHandler.initialize() veaf.loggers.get(veafEventHandler.Id):debug("veafEventHandler.initialize()") -- copy the events maps (add events by name) and add the events by id to the veafEventHandler.knownEventsNames table veafEventHandler.knownEventsNames = {} veafEventHandler.knownEvents = {} for eventId, event in pairs(veafEventHandler.EVENTS) do veaf.loggers.get(veafEventHandler.Id):trace("eventId=%s, event=%s", eventId, event) veafEventHandler.knownEvents[event.name] = event veafEventHandler.knownEvents[eventId] = event veafEventHandler.knownEventsNames[eventId] = event.name end veaf.loggers.get(veafEventHandler.Id):trace("veafEventHandler.knownEvents=%s", veafEventHandler.knownEvents) veaf.loggers.get(veafEventHandler.Id):trace("veafEventHandler.knownEventsNames=%s", veafEventHandler.knownEventsNames) -- Add event handler. world.addEventHandler(veafEventHandler.eventHandler) end veafEventHandler.initialize() ------------------ END script veafEventHandler.lua ------------------ ------------------ START script veafMarkers.lua ------------------ ------------------------------------------------------------------ -- VEAF markers function library for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Listen to marker events and execute event handlers. -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafMarkers = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafMarkers.Id = "MARKERS" --- Version. veafMarkers.Version = "1.1.0" --- DCS bug regarding wrong marker vector components was fixed. If so, set to true! veafMarkers.DCSbugfixed = true -- trace level, specific to this module --veafMarkers.LogLevel = "trace" veaf.loggers.new(veafMarkers.Id, veafMarkers.LogLevel) veafMarkers.MarkerAdd = 1 veafMarkers.MarkerChange = 2 veafMarkers.MarkerRemove = 3 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafMarkers.eventHandlerId = 0 veafMarkers.onEventMarkChangeEventHandlers = {} veafMarkers.onEventMarkAddEventHandlers = {} veafMarkers.onEventMarkRemoveEventHandlers = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Event handler. veafMarkers.eventHandler = {} --- Handle world events. function veafMarkers.eventHandler:onEvent(Event) -- Only interested in S_EVENT_MARK_* if Event == nil or Event.idx == nil then return true end -- Debug output. if Event.id == world.event.S_EVENT_MARK_ADDED then veaf.loggers.get(veafMarkers.Id):debug("S_EVENT_MARK_ADDED") elseif Event.id == world.event.S_EVENT_MARK_CHANGE then veaf.loggers.get(veafMarkers.Id):debug("S_EVENT_MARK_CHANGE") elseif Event.id == world.event.S_EVENT_MARK_REMOVED then veaf.loggers.get(veafMarkers.Id):debug("S_EVENT_MARK_REMOVED") end veaf.loggers.get(veafMarkers.Id):trace(string.format("Event id = %s", tostring(Event.id))) veaf.loggers.get(veafMarkers.Id):trace(string.format("Event time = %s", tostring(Event.time))) veaf.loggers.get(veafMarkers.Id):trace(string.format("Event idx = %s", tostring(Event.idx))) veaf.loggers.get(veafMarkers.Id):trace(string.format("Event coalition = %s", tostring(Event.coalition))) veaf.loggers.get(veafMarkers.Id):trace(string.format("Event group id = %s", tostring(Event.groupID))) veaf.loggers.get(veafMarkers.Id):trace(string.format("Event pos X = %s", tostring(Event.pos.x))) veaf.loggers.get(veafMarkers.Id):trace(string.format("Event pos Y = %s", tostring(Event.pos.y))) veaf.loggers.get(veafMarkers.Id):trace(string.format("Event pos Z = %s", tostring(Event.pos.z))) if Event.initiator ~= nil and Event.initiator.getName then local _unitname = Event.initiator:getName() veaf.loggers.get(veafMarkers.Id):trace(string.format("Event ini unit = %s", tostring(_unitname))) end veaf.loggers.get(veafMarkers.Id):trace(string.format("Event text = \n%s", tostring(Event.text))) -- Call event function when a marker has changed, i.e. text was entered or changed. if Event.id == world.event.S_EVENT_MARK_CHANGE then veafMarkers.onEvent(Event, veafMarkers.onEventMarkChangeEventHandlers) elseif Event.id == world.event.S_EVENT_MARK_ADDED then veafMarkers.onEvent(Event, veafMarkers.onEventMarkAddEventHandlers) elseif Event.id == world.event.S_EVENT_MARK_REMOVED then veafMarkers.onEvent(Event, veafMarkers.onEventMarkRemoveEventHandlers) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function executed when a marker event occurs. function veafMarkers.onEvent(event, eventHandlersTable) local vec3 = nil -- Check if marker has a text and the veafMarkers.keyphrase keyphrase. if event.text ~= nil then -- browse all the event handlers registered for this type of event for i = 1, #eventHandlersTable do -- compute the event position if not already done if vec3 == nil then if veafMarkers.DCSbugfixed then vec3 = {x = event.pos.x, y = event.pos.y, z = event.pos.z} else -- Convert (wrong x-->z, z-->x) vec3 vec3 = {x = event.pos.z, y = event.pos.y, z = event.pos.x} end -- By default, alt of mark point is always 5 m! Adjust for the correct ASL height. vec3.y = veaf.getLandHeight(vec3) end -- call the event handler local eventHandler = eventHandlersTable[i] veaf.loggers.get(veafMarkers.Id):debug("Calling eventHandler #" .. eventHandler.id) local err, errmsg = pcall(eventHandler.f, vec3, event) if not err then veaf.loggers.get(veafMarkers.Id):error('Error in event handler #' .. eventHandler.id .. ' : '.. errmsg) end veaf.loggers.get(veafMarkers.Id):debug("Returning after eventHandler #" .. eventHandler.id) end end end --- Register an event handler -- @tparam function f event handling function (the first parameter is always the event position, and the second is the event) -- @treturn number event handler id. function veafMarkers.registerEventHandler(eventType, eventHandler) --verify correct types assert(type(eventHandler) == 'function', 'variable 1, expected function, got ' .. type(eventHandler)) if not vars then vars = {} end veafMarkers.eventHandlerId = veafMarkers.eventHandlerId + 1 if eventType == veafMarkers.MarkerAdd then table.insert(veafMarkers.onEventMarkAddEventHandlers, {f = eventHandler, id = veafMarkers.eventHandlerId}) elseif eventType == veafMarkers.MarkerChange then table.insert(veafMarkers.onEventMarkChangeEventHandlers, {f = eventHandler, id = veafMarkers.eventHandlerId}) elseif eventType == veafMarkers.MarkerRemove then table.insert(veafMarkers.onEventMarkRemoveEventHandlers, {f = eventHandler, id = veafMarkers.eventHandlerId}) else -- wrong event type end return veafMarkers.eventHandlerId end function veafMarkers.removeItemFromList(list, id) local i = 1 while i <= #list do if list[i].id == id then table.remove(list, i) return true else i = i + 1 end end return false end --- Removes an event handler -- @tparam number id event handler id -- @treturn boolean true if event handler was successfully removed, false otherwise. function veafMarkers.unregisterEventHandler(id) local result = veafMarkers.removeItemFromList(veafMarkers.onEventMarkAddEventHandlers, id) if not(result) then result = veafMarkers.removeItemFromList(veafMarkers.onEventMarkChangeEventHandlers, id) end if not(result) then result = veafMarkers.removeItemFromList(veafMarkers.onEventMarkRemoveEventHandlers, id) end return result end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Add event handler. world.addEventHandler(veafMarkers.eventHandler) --- Enable/Disable error boxes displayed on screen. env.setErrorMessageBoxEnabled(false) veaf.loggers.get(veafMarkers.Id):info(string.format("Loading version %s", veafMarkers.Version)) ------------------ END script veafMarkers.lua ------------------ ------------------ START script veafInterpreter.lua ------------------ ------------------------------------------------------------------ -- VEAF interpreter for DCS World -- By Zip (2019) -- -- Features: -- --------- -- * interprets a command and a position, and executes one of the VEAF script commands as if it had been requested in a map marker -- * Possibilities : -- * - at mission start, have pre-placed units trigger specific commands -- * - serve as a base for activating commands in Combat Zones (see veafCombatZone.lua) -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ --- veafInterpreter Table. veafInterpreter = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafInterpreter.Id = "INTERPRETER" --- Version. veafInterpreter.Version = "1.6.2" -- trace level, specific to this module --veafInterpreter.LogLevel = "trace" veaf.loggers.new(veafInterpreter.Id, veafInterpreter.LogLevel) --- Key phrase to look for in the unit name which triggers the interpreter. veafInterpreter.Starter = "#veafInterpreter%[\"" veafInterpreter.Trailer = "\"%]" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- delay before the mission editor unit names are interpreted veafInterpreter.DelayForStartup = 1 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Analyse the text ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafInterpreter.interpret(text) veaf.loggers.get(veafInterpreter.Id):trace(string.format("veafInterpreter.interpret([%s])",text)) local result = nil local p1, p2 = text:find(veafInterpreter.Starter) if p2 then -- starter has been found text = text:sub(p2 + 1) p1, p2 = text:find(veafInterpreter.Trailer) if p1 then -- trailer has been found result = text:sub(1, p1 - 1) end end return result end function veafInterpreter.execute(command, position, coalition, route, spawnedGroups) local function logDebug(message) veaf.loggers.get(veafInterpreter.Id):debug(message) return true end if command == nil then return end if position == nil then return end veaf.loggers.get(veafInterpreter.Id):trace("veafInterpreter.execute([%s],[%s])", command, position) local commandExecuted = false spawnedGroups = spawnedGroups or {} if logDebug("checking in veafShortcuts") and veafShortcuts.executeCommand(position, command, coalition, nil, true, spawnedGroups, route) then return true elseif logDebug("checking in veafSpawn") and veafSpawn.executeCommand(position, command, coalition, nil, true, spawnedGroups, nil, nil, route, true) then return true elseif logDebug("checking in veafNamedPoints") and veafNamedPoints.executeCommand(position, {text=command, coalition=-1}, true) then return true elseif logDebug("checking in veafCasMission") and veafCasMission.executeCommand(position, command, coalition, true) then return true elseif logDebug("checking in veafSecurity") and veafSecurity.executeCommand(position, command, true) then return true elseif logDebug("checking in veafMove") and veafMove.executeCommand(position, command, true) then return true elseif logDebug("checking in veafRadio") and veafRadio.executeCommand(position, command, coalition, true) then return true elseif logDebug("checking in veafRemote") and veafRemote.executeCommand(position, command) then return true else return false end end function veafInterpreter.executeCommandOnUnit(unitName, command) if command then -- found an interpretable command veaf.loggers.get(veafInterpreter.Id):debug(string.format("found an interpretable command : [%s]", command)) local unit = Unit.getByName(unitName) if unit then local position = unit:getPosition().p veaf.loggers.get(veafInterpreter.Id):trace(string.format("found the unit at : [%s]", veaf.vecToString(position))) local groupName = unit:getGroup():getName() veaf.loggers.get(veafInterpreter.Id):debug(string.format("in [%s]", groupName)) local route = mist.getGroupRoute(groupName, 'task') veaf.loggers.get(veafInterpreter.Id):trace(string.format("route = [%s]", veaf.p(route))) if veafInterpreter.execute(command, position, unit:getCoalition(), route, nil) then unit:getGroup():destroy() end else -- it may be a static instead of a unit local static = StaticObject.getByName(unitName) if static then local position = static:getPosition().p veaf.loggers.get(veafInterpreter.Id):trace("found the static at : [%s]", veaf.vecToString(position)) if veafInterpreter.execute(command, position, static:getCoalition(), nil, nil) then static:destroy() end end end end end function veafInterpreter.processObject(unitName) veaf.loggers.get(veafInterpreter.Id):trace(string.format("veafInterpreter.processObject([%s])", unitName)) local command = veafInterpreter.interpret(unitName) veafInterpreter.executeCommandOnUnit(unitName, command) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafInterpreter.initialize() mist.scheduleFunction(veafInterpreter._initialize, {}, timer.getTime()+veafInterpreter.DelayForStartup) end function veafInterpreter._initialize() -- the following code is liberally adapted from MiST (thanks Grimes !) local l_units = mist.DBs.units --local reference for faster execution for coa, coa_tbl in pairs(l_units) do for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do if type(unit_type_tbl) == 'table' then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, mist_unit in pairs(group_tbl.units) do local unitName = mist_unit.unitName veaf.loggers.get(veafInterpreter.Id):trace(string.format("initialize - checking unit [%s]", unitName)) veafInterpreter.processObject(unitName) end end end end end end end end veaf.loggers.get(veafInterpreter.Id):info(string.format("Loading version %s", veafInterpreter.Version)) ------------------ END script veafInterpreter.lua ------------------ ------------------ START script veafRadio.lua ------------------ ------------------------------------------------------------------ -- VEAF radio menu script library for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Manage the VEAF radio menus in the F10 - Other menu -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ --- veafRadio Table. veafRadio = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafRadio.Id = "RADIO" --- Version. veafRadio.Version = "1.3.0" -- trace level, specific to this module --veafRadio.LogLevel = "trace" veaf.loggers.new(veafRadio.Id, veafRadio.LogLevel) veafRadio.RadioMenuName = "VEAF" -- constants used to determine how the radio menu is set up veafRadio.USAGE_ForAll = 0 veafRadio.USAGE_ForGroup = 1 veafRadio.USAGE_ForUnit = 2 -- delay for the actual refresh veafRadio.refreshRadioMenu_DELAY = 1 --- Key phrase to look for in the mark text which triggers the command. veafRadio.Keyphrase = "_radio" --- number of seconds between beacons checks veafRadio.BEACONS_SCHEDULE = 5 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafRadio.skipHelpMenus = true --- Humans Units (associative array unitName => unit) veafRadio.humanUnits = {} veafRadio.humanGroups = {} --- This structure contains all the radio menus veafRadio.radioMenu = {} veafRadio.radioMenu.title = veafRadio.RadioMenuName veafRadio.radioMenu.dcsRadioMenu = nil veafRadio.radioMenu.subMenus = {} veafRadio.radioMenu.commands = {} --- Counts the size of the radio menu veafRadio.radioMenuSize = {} veafRadio.beacons = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafRadio.onBirthEvent(event) veaf.loggers.get(veafRadio.Id):trace("veafRadio.onBirthEvent(%s)", event) -- find the originator unit local unitName = event and event.initiator and event.initiator.unitName if not unitName then return end veaf.loggers.get(veafRadio.Id):trace("unitName=%s", unitName) if mist.DBs.humansByName[unitName] then -- it's a human unit veaf.loggers.get(veafRadio.Id):trace("veafRadio.humanUnits=%s", veafRadio.humanUnits) veaf.loggers.get(veafRadio.Id):trace("unitName %s is a human unit", unitName) if not veafRadio.humanUnits[unitName] then -- add the unit to the human units list and rebuild the radio menu veaf.loggers.get(veafRadio.Id):trace("Adding human unit %s", unitName) local groupId = event and event.initiator and event.initiator.unitGroupId local callsign = event and event.initiator and event.initiator.unitPilotName if not callsign then callsign = event and event.initiator and event.initiator.unitCallsign end local unitObject = {name=unitName, spawned=true, groupId=groupId, callsign=callsign} veafRadio.humanUnits[unitName] = {} veafRadio.humanUnits[unitName].spawned = true veafRadio.humanUnits[unitName] = unitObject veaf.loggers.get(veafRadio.Id):trace("veafRadio.humanGroups=%s", veafRadio.humanGroups) if not veafRadio.humanGroups[groupId] then veafRadio.humanGroups[groupId] = {} veafRadio.humanGroups[groupId].callsigns = {} veafRadio.humanGroups[groupId].units = {} end table.insert(veafRadio.humanGroups[groupId].callsigns, callsign) veaf.loggers.get(veafRadio.Id):trace("veafRadio.humanGroups=%s", veafRadio.humanGroups) veafRadio.humanGroups[groupId].units[callsign] = unitObject -- sort callsigns for each group for _, groupData in pairs(veafRadio.humanGroups) do table.sort(groupData.callsigns) end -- refresh the radio menu veaf.loggers.get(veafRadio.Id):debug("refreshRadioMenu() following event %s of human unit %s", event.type and event.type.name, unitName) veafRadio.refreshRadioMenu() end end end --- Function executed when a mark has changed. This happens when text is entered or changed. function veafRadio.onEventMarkChange(eventPos, event) if veafRadio.executeCommand(eventPos, event.text, event.coalition) then -- Delete old mark. veaf.loggers.get(veafRadio.Id):trace(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafRadio.executeCommand(eventPos, eventText, eventCoalition, bypassSecurity) veaf.loggers.get(veafRadio.Id):trace(string.format("veafRadio.executeCommand(%s)", eventText)) -- Check if marker has a text and the veafRadio.keyphrase keyphrase. if eventText ~= nil and eventText:lower():find(veafRadio.Keyphrase) then -- Analyse the mark point text and extract the keywords. local options = veafRadio.markTextAnalysis(eventText) if options then veaf.loggers.get(veafRadio.Id):trace(string.format("options.path=%s",veaf.p(options.path))) -- Check options commands if options.transmit and options.message and options.frequencies and options.name then -- transmit a radio message via SRS veafRadio.transmitMessage(options.message, options.frequencies, options.modulations, options.name, eventCoalition, eventPos, options.quiet) return true elseif options.playmp3 and options.path and options.frequencies and options.name then -- play a MP3 file via SRS veafRadio.playToRadio(options.path, options.frequencies, options.modulations, options.name, eventCoalition, eventPos, options.quiet) return true end else -- None of the keywords matched. return false end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Analyse the mark text and extract keywords. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Extract keywords from mark text. function veafRadio.markTextAnalysis(text) veaf.loggers.get(veafRadio.Id):trace(string.format("markTextAnalysis(%s)", text)) -- Option parameters extracted from the mark text. local switch = {} switch.transmit = false switch.playmp3 = false switch.message = nil switch.frequencies = "251" switch.modulations = "AM" switch.name = "SRS" switch.quiet = false switch.path = nil -- Check for correct keywords. if text:lower():find(veafRadio.Keyphrase .. " transmit") then switch.transmit = true elseif text:lower():find(veafRadio.Keyphrase .. " play") then switch.playmp3 = true else return nil end -- keywords are split by "," local keywords = veaf.split(text, ",") for _, keyphrase in pairs(keywords) do -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. local str = veaf.breakString(veaf.trim(keyphrase), " ") local key = str[1] local val = str[2] if key:lower() == "message" then -- Set message. veaf.loggers.get(veafRadio.Id):trace(string.format("Keyword message = %s", tostring(val))) switch.message = val elseif key:lower() == "path" then -- Set path. veaf.loggers.get(veafRadio.Id):trace(string.format("Keyword path = %s", tostring(val))) switch.path = val elseif key:lower() == "name" then -- Set name. veaf.loggers.get(veafRadio.Id):trace(string.format("Keyword name = %s", tostring(val))) switch.name = val elseif key:lower() == "quiet" then -- Set quiet. veaf.loggers.get(veafRadio.Id):trace("Keyword quiet found") switch.quiet = true elseif key:lower() == "freq" or key:lower() == "freqs" or key:lower() == "frequency" or key:lower() == "frequencies" then -- Set frequencies. veaf.loggers.get(veafRadio.Id):trace(string.format("Keyword frequencies = %s", tostring(val))) switch.frequencies = val elseif key:lower() == "mod" or key:lower() == "mods" or key:lower() == "modulation" or key:lower() == "modulations" then -- Set modulations. veaf.loggers.get(veafRadio.Id):trace(string.format("Keyword modulations = %s", tostring(val))) switch.modulations = val elseif key:lower() == "path" then -- Set path. veaf.loggers.get(veafRadio.Id):trace(string.format("Keyword path = %s", tostring(val))) switch.path = val end end return switch end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Main event handler (used for PLAYER ENTER UNIT events) ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafRadio._proxyMethod(parameters) veaf.loggers.get(veafRadio.Id):trace("parameters="..veaf.p(parameters)) local realMethod, realParameters = veaf.safeUnpack(parameters) veaf.loggers.get(veafRadio.Id):trace("realMethod="..veaf.p(realMethod)) veaf.loggers.get(veafRadio.Id):trace("realParameters="..veaf.p(realParameters)) if veafSecurity.isAuthenticated() then realMethod(realParameters) else veaf.loggers.get(veafRadio.Id):error("Your radio has to be authenticated for '+'' commands") trigger.action.outText("Your radio has to be authenticated for '+'' commands", 5) end end --- Refresh the radio menu, based on stored information --- This is called from another method that has first changed the radio menu information by adding or removing elements function veafRadio.refreshRadioMenu(dontDelay) veaf.loggers.get(veafRadio.Id):debug(string.format("veafRadio.refreshRadioMenu()")) -- delay the refresh if possible if not dontDelay then if not veafRadio.refreshRadioMenuDelayedScheduling then veafRadio.refreshRadioMenuDelayedScheduling = mist.scheduleFunction(veafRadio._refreshRadioMenu,{},timer.getTime()+veafRadio.refreshRadioMenu_DELAY) end else veafRadio._refreshRadioMenu() end end --- actually refresh the radio menu, based on stored information function veafRadio._refreshRadioMenu() veaf.loggers.get(veafRadio.Id):debug(string.format("veafRadio._refreshRadioMenu()")) veafRadio.refreshRadioMenuDelayedScheduling = nil -- completely delete the dcs radio menu veaf.loggers.get(veafRadio.Id):trace("completely delete the dcs radio menu") if veafRadio.radioMenu.dcsRadioMenu then missionCommands.removeItem(veafRadio.radioMenu.dcsRadioMenu) else veaf.loggers.get(veafRadio.Id):info("_refreshRadioMenu() first time : no DCS radio menu yet") end -- create all the commands and submenus in the dcs radio menu veaf.loggers.get(veafRadio.Id):trace("create all the commands and submenus in the dcs radio menu") veafRadio.refreshRadioSubmenu(nil, veafRadio.radioMenu) end function veafRadio._addCommand(groupId, title, menu, command, parameters) if not command.method then veaf.loggers.get(veafRadio.Id):error("ERROR - missing method for command " .. title) end local _title = title local _method = command.method local _parameters = parameters if command.isSecured then veaf.loggers.get(veafRadio.Id):trace("adding secured command") _method = veafRadio._proxyMethod _parameters = {command.method, _parameters} if veafSecurity.isAuthenticated() then _title = "-" .. title else _title = "+" .. title end end veaf.loggers.get(veafRadio.Id):trace("_title=%s", veaf.p(_title)) veaf.loggers.get(veafRadio.Id):trace("_parameters=%s", veaf.p(_parameters)) if groupId then veaf.loggers.get(veafRadio.Id):trace("adding for group %s command %s",groupId or "", _title or "") missionCommands.addCommandForGroup(groupId, _title, menu, _method, _parameters) else veaf.loggers.get(veafRadio.Id):trace("adding for all command %s",_title or "") missionCommands.addCommand(_title, menu, _method, _parameters) end end function veafRadio.refreshRadioSubmenu(parentRadioMenu, radioMenu) veaf.loggers.get(veafRadio.Id):debug("veafRadio.refreshRadioSubmenu %s", veaf.p(veaf.ifnn(radioMenu, "title"))) if not radioMenu or not radioMenu.title then return end -- create the radio menu in DCS if parentRadioMenu then radioMenu.dcsRadioMenu = missionCommands.addSubMenu(radioMenu.title, parentRadioMenu.dcsRadioMenu) else radioMenu.dcsRadioMenu = missionCommands.addSubMenu(radioMenu.title) end -- create the commands in the radio menu table.sort(radioMenu.commands, function(a, b) if a.title and b.title then return a.title < b.title else return false end end) for count = 1,#radioMenu.commands do local command = radioMenu.commands[count] veaf.loggers.get(veafRadio.Id):trace(string.format("command=%s",veaf.p(command))) if not command.usage then command.usage = veafRadio.USAGE_ForAll end if command.usage ~= veafRadio.USAGE_ForAll then -- build menu for each player group local alreadyDoneGroups = {} for groupId, groupData in pairs(veafRadio.humanGroups) do veaf.loggers.get(veafRadio.Id):trace(string.format("groupId=%s",veaf.p(groupId))) for _, callsign in pairs(groupData.callsigns) do veaf.loggers.get(veafRadio.Id):trace(string.format("callsign=%s",veaf.p(callsign))) local unitData = groupData.units[callsign] local unitName = unitData.name veaf.loggers.get(veafRadio.Id):trace(string.format("unitName=%s",veaf.p(unitName))) local humanUnit = veafRadio.humanUnits[unitName] veaf.loggers.get(veafRadio.Id):trace(string.format("humanUnit=%s",veaf.p(humanUnit))) if humanUnit and humanUnit.spawned then veaf.loggers.get(veafRadio.Id):debug(string.format("add radio command for player unit %s",veaf.p(unitName))) -- add radio command by player unit or group local parameters = command.parameters if parameters == nil then parameters = unitName else parameters = { command.parameters } table.insert(parameters, unitName) end local _title = command.title if command.usage == veafRadio.USAGE_ForUnit then _title = callsign .. " - " .. command.title end if alreadyDoneGroups[groupId] == nil or command.usage == veafRadio.USAGE_ForUnit then veafRadio._addCommand(groupId, _title, radioMenu.dcsRadioMenu, command, parameters) end alreadyDoneGroups[groupId] = true end end end else veafRadio._addCommand(nil, command.title, radioMenu.dcsRadioMenu, command, command.parameters) end end -- recurse to create the submenus in the radio menu table.sort(radioMenu.subMenus, function(a, b) if a.title and b.title then return a.title < b.title else return false end end) for count = 1,#radioMenu.subMenus do local subMenu = radioMenu.subMenus[count] veafRadio.refreshRadioSubmenu(radioMenu, subMenu) end end function veafRadio.addCommandToMainMenu(title, method) return veafRadio._addCommandToMainMenu(title, method, false) end function veafRadio.addSecuredCommandToMainMenu(title, method) return veafRadio._addCommandToMainMenu(title, method, true) end function veafRadio._addCommandToMainMenu(title, method, isSecured) return veafRadio._addCommandToSubmenu(title, nil, method, nil, nil, isSecured) end function veafRadio.addCommandToSubmenu(title, radioMenu, method, parameters, usage) return veafRadio._addCommandToSubmenu(title, radioMenu, method, parameters, usage, false) end function veafRadio.addSecuredCommandToSubmenu(title, radioMenu, method, parameters, usage) return veafRadio._addCommandToSubmenu(title, radioMenu, method, parameters, usage, true) end function veafRadio._addCommandToSubmenu(title, radioMenu, method, parameters, usage, isSecured) veaf.loggers.get(veafRadio.Id):debug(string.format("_addCommandToSubmenu(%s)",veaf.p(title))) local command = {} command.title = title command.method = method command.parameters = parameters command.isSecured = isSecured command.usage = usage if command.usage == nil then command.usage = veafRadio.USAGE_ForAll end local menu = veafRadio.radioMenu if radioMenu then menu = radioMenu end -- add command to menu table.insert(menu.commands, command) return command end function veafRadio.delCommand(radioMenu, title) for count = 1,#radioMenu.commands do local command = radioMenu.commands[count] if command.title == title then table.remove(radioMenu.commands, count) return true end end return false end function veafRadio.addMenu(title) return veafRadio.addSubMenu(title, nil) end function veafRadio.addSubMenu(title, radioMenu) local subMenu = {} subMenu.title = title subMenu.dcsRadioMenu = nil subMenu.subMenus = {} subMenu.commands = {} local menu = veafRadio.radioMenu if radioMenu then menu = radioMenu end -- add subMenu to menu table.insert(menu.subMenus, subMenu) return subMenu end function veafRadio.clearSubmenu(subMenu) if not subMenu then veaf.loggers.get(veafRadio.Id):error("veafRadio.clearSubmenu() subMenu parameter is nil !") return end veaf.loggers.get(veafRadio.Id):debug(string.format("veafRadio.clearSubmenu(%s)",subMenu.title)) subMenu.subMenus = {} subMenu.commands = {} end function veafRadio.delSubmenu(subMenu, radioMenu) if not subMenu then veaf.loggers.get(veafRadio.Id):error("veafRadio.delSubmenu() subMenu parameter is nil !") return end local menu = veafRadio.radioMenu if radioMenu then menu = radioMenu end veaf.arrayRemoveWhen(menu.subMenus, function(t, i, j) -- Return true to keep the value, or false to discard it. --veaf.loggers.get(veafRadio.Id):trace("searching for " .. subMenu.title) local v = menu.subMenus[i] --veaf.loggers.get(veafRadio.Id):trace("checking " .. v.title) if v == subMenu or v.title == subMenu then --veaf.loggers.get(veafRadio.Id):trace("found ! removing " .. v.title) return false else --veaf.loggers.get(veafRadio.Id):trace("keeping " .. v.title) return true end end); end -- build a paginated submenu (internal paginating method) local function _buildRadioMenuPage(menu, titles, elementsByTitle, addCommandToSubmenuMethod, pageSize, startIndex) veaf.loggers.get(veafRadio.Id):trace(string.format("_buildRadioMenuPage(pageSize=%s, startIndex=%s)",tostring(pageSize), tostring(startIndex))) local titlesCount = #titles veaf.loggers.get(veafRadio.Id):trace(string.format("titlesCount = %d",titlesCount)) local pageSize = pageSize if not pageSize then pageSize = 10 end local endIndex = titlesCount if endIndex - startIndex >= pageSize then endIndex = startIndex + pageSize - 2 end veaf.loggers.get(veafRadio.Id):trace(string.format("endIndex = %d",endIndex)) veaf.loggers.get(veafRadio.Id):trace(string.format("adding commands from %d to %d",startIndex, endIndex)) for index = startIndex, endIndex do local title = titles[index] veaf.loggers.get(veafRadio.Id):trace(string.format("titles[%d] = %s",index, title)) local element = elementsByTitle[title] addCommandToSubmenuMethod(menu, title, element) end if endIndex < titlesCount then veaf.loggers.get(veafRadio.Id):trace("adding next page menu") local nextPageMenu = veafRadio.addSubMenu("Next page", menu) _buildRadioMenuPage(nextPageMenu, titles, elementsByTitle, addCommandToSubmenuMethod, 10, endIndex+1) end end -- build a paginated submenu (main method) function veafRadio.addPaginatedRadioElements(radioMenu, addCommandToSubmenuMethod, elements, titleAttribute, sortAttribute) veaf.loggers.get(veafRadio.Id):trace(string.format("veafRadio.addPaginatedRadioElements() : elements=%s",veaf.p(elements))) if not addCommandToSubmenuMethod then veaf.loggers.get(veafRadio.Id):error("veafRadio.addPaginatedRadioMenu : addCommandToSubmenuMethod is mandatory !") return end local pageSize = 10 - #radioMenu.commands local sortedElements = {} local sortAttribute = sortAttribute or "sort" local titleAttribute = titleAttribute or "title" for name, element in pairs(elements) do local sortValue = element[sortAttribute] if not sortValue then sortValue = name end table.insert(sortedElements, {element=element, sort=sortValue, title=name}) end local compare = function(a,b) if not(a) then a = {} end if not(a["sort"]) then a["sort"] = 0 end if not(b) then b = {} end if not(b["sort"]) then b["sort"] = 0 end return a["sort"] < b["sort"] end table.sort(sortedElements, compare) local sortedTitles = {} local elementsByTitle = {} for i = 1, #sortedElements do local title = sortedElements[i].element[titleAttribute] if not title then title = sortedElements[i].title end table.insert(sortedTitles, title) elementsByTitle[title] = sortedElements[i].element end veaf.loggers.get(veafRadio.Id):trace("sortedTitles="..veaf.p(sortedTitles)) _buildRadioMenuPage(radioMenu, sortedTitles, elementsByTitle, addCommandToSubmenuMethod, pageSize, 1) --veafRadio.refreshRadioMenu() end -- build a paginated submenu (main method) function veafRadio.addPaginatedRadioMenu(title, radioMenu, addCommandToSubmenuMethod, elements, titleAttribute, sortAttribute) veaf.loggers.get(veafRadio.Id):trace(string.format("veafRadio.addPaginatedRadioMenu(title=%s)",title)) local firstPagePath = veafRadio.addSubMenu(title, radioMenu) veafRadio.addPaginatedRadioElements(firstPagePath, addCommandToSubmenuMethod, elements, titleAttribute, sortAttribute) return firstPagePath end function veafRadio.getHumanUnitOrWingman(unitName) local result = Unit.getByName(unitName) if not result then local unitData = veafRadio.humanUnits[unitName] veaf.loggers.get(veafRadio.Id):trace(string.format("unitData=%s",veaf.p(unitData))) if unitData and unitData.groupId then local mistGroup = mist.DBs.groupsById[unitData.groupId] veaf.loggers.get(veafRadio.Id):trace(string.format("mistGroup=%s",veaf.p(mistGroup))) if mistGroup then local group = Group.getByName(mistGroup.groupName) if group then veaf.loggers.get(veafRadio.Id):trace(string.format("group=%s",veaf.p(group))) veaf.loggers.get(veafRadio.Id):trace(string.format("group:getUnits()=%s",veaf.p(group:getUnits()))) for _, groupUnit in pairs(group:getUnits()) do if not result then result = groupUnit end end end end end end if result then veaf.loggers.get(veafRadio.Id):trace(string.format("result=%s",veaf.p(result))) veaf.loggers.get(veafRadio.Id):trace(string.format("result:getName()=%s",veaf.p(result:getName()))) end return result end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- radio beacons ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafRadio.startBeacon(name, firstRunDelay, secondsBetweenRepeats, frequencies, modulations, message, mp3, coalition) veaf.loggers.get(veafRadio.Id):debug("startBeacon(name=%s, firstRunDelay=%s, secondsBetweenRepeats=%s, coalition=%s, frequencies=%s, modulations=%s, message=%s, mp3=%s)", veaf.p(name), veaf.p(firstRunDelay), veaf.p(secondsBetweenRepeats), veaf.p(coalition), veaf.p(frequencies), veaf.p(modulations), veaf.p(message), veaf.p(mp3)) local beacon = veafRadio.beacons[name:lower()] if not beacon then beacon = {} end beacon.name = name beacon.secondsBetweenRepeats = secondsBetweenRepeats beacon.nextRun = timer.getTime()+firstRunDelay beacon.frequencies = frequencies beacon.modulations = modulations beacon.coalition = coalition beacon.message = message beacon.mp3 = mp3 veaf.loggers.get(veafRadio.Id):debug(string.format("adding beacon %s", tostring(name))) veafRadio.beacons[name:lower()] = beacon end function veafRadio._runBeacons() --veaf.loggers.get(veafRadio.Id):trace("_runBeacons()") local now = timer.getTime() --veaf.loggers.get(veafRadio.Id):debug(string.format("now = %s", tostring(now))) for name, beacon in pairs(veafRadio.beacons) do --veaf.loggers.get(veafRadio.Id):trace(string.format("checking %s supposed to run at %s", tostring(beacon.name), tostring(beacon.nextRun))) if beacon.nextRun <= now then --veaf.loggers.get(veafRadio.Id):trace(string.format("running beacon %s", tostring(name))) if beacon.message then veafRadio.transmitMessage(beacon.message, beacon.frequencies, beacon.modulations, beacon.name, beacon.coalition, nil, true) elseif beacon.mp3 then veafRadio.playToRadio(beacon.mp3, beacon.frequencies, beacon.modulations, beacon.name, beacon.coalition, nil, true) end beacon.nextRun = now + beacon.secondsBetweenRepeats end end --veaf.loggers.get(veafRadio.Id):trace(string.format("rescheduling in %s seconds", tostring(veafRadio.BEACONS_SCHEDULE))) mist.scheduleFunction(veafRadio._runBeacons,{},timer.getTime()+veafRadio.BEACONS_SCHEDULE) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- radio utilities ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- transmit a radio message or play a mp3 file via SRS function veafRadio._transmitViaSRS(message, file, frequencies, modulations, name, coalition, eventPos) veaf.loggers.get(veafRadio.Id):debug("transmitMessage(name=%s, coalition=%s, frequencies=%s, modulations=%s, message=%s, file=%s)", veaf.p(name), veaf.p(coalition), veaf.p(frequencies), veaf.p(modulations), veaf.p(message), veaf.p(file)) local posOption = "" if eventPos then veaf.loggers.get(veafRadio.Id):trace(string.format("eventPos=%s",veaf.p(eventPos))) local lat, lon, alt = coord.LOtoLL(eventPos) posOption = string.format("-L %d -O %d -A %d", lat, lon, alt) end local contentOption = "" if message then contentOption = string.format("-t \"%s\"", message) elseif file then contentOption = string.format("-i \"%s\"", file) else veaf.loggers.get(veafRadio.Id):error("no message nor file for veafRadio._transmitViaSRS()!") return end local l_os = os if not l_os and SERVER_CONFIG and SERVER_CONFIG.getModule then l_os = SERVER_CONFIG.getModule("os") end if l_os and STTS then local cmd = string.format("start /min \"%s\" \"%s\\%s\" %s -f %s -m %s -c %s -p %s -n \"%s\" %s", STTS.DIRECTORY, STTS.DIRECTORY, STTS.EXECUTABLE, contentOption, frequencies, modulations, coalition, STTS.SRS_PORT, name, posOption) veaf.loggers.get(veafRadio.Id):trace(string.format("executing os command %s", cmd)) local result = l_os.execute(cmd) if result == nil then veaf.loggers.get(veafRadio.Id):warn(string.format("Nil result after executing os command %s", cmd)) end return result end end -- transmit a radio message via SRS function veafRadio.transmitMessage(message, frequencies, modulations, name, coalition, eventPos, quiet) veaf.loggers.get(veafRadio.Id):debug("transmitMessage(name=%s, coalition=%s, frequencies=%s, modulations=%s, message=%s)", veaf.p(name), veaf.p(coalition), veaf.p(frequencies), veaf.p(modulations), veaf.p(message)) if eventPos then veaf.loggers.get(veafRadio.Id):trace(string.format("eventPos=%s",veaf.p(eventPos))) end veafRadio._transmitViaSRS(message, nil, frequencies, modulations, name, coalition, eventPos) if not quiet and coalition then trigger.action.outTextForCoalition(coalition, string.format("%s (%s) : %s", name, frequencies, message), 30) end end -- play a MP3 file via SRS function veafRadio.playToRadio(pathToMP3, frequencies, modulations, name, coalition, eventPos, quiet) veaf.loggers.get(veafRadio.Id):debug("playToRadio(name=%s, coalition=%s, frequencies=%s, modulations=%s, pathToMP3=%s)", veaf.p(name), veaf.p(coalition), veaf.p(frequencies), veaf.p(modulations), veaf.p(pathToMP3)) if eventPos then veaf.loggers.get(veafRadio.Id):trace(string.format("eventPos=%s",veaf.p(eventPos))) end veafRadio._transmitViaSRS(nil, pathToMP3, frequencies, modulations, name, coalition, eventPos) if not quiet and coalition then trigger.action.outTextForCoalition(coalition, string.format("%s (%s) : playing %s", name, frequencies, pathToMP3), 30) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- user menus ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafRadio.createUserMenu(configuration, groupId) veaf.loggers.get(veafRadio.Id):debug("veafRadio.createUserMenu(groupId=%s, configuration=%s)",veaf.p(groupId), veaf.p(configuration)) local function _recursivelyCreateMenu(configuration, parentMenu) veaf.loggers.get(veafRadio.Id):trace("_recursivelyCreateMenu(configuration=%s, parentMenu=%s)",veaf.p(configuration), veaf.p(parentMenu)) local result for _, item in pairs(configuration) do local itemType = item[1] veaf.loggers.get(veafRadio.Id):trace("itemType = [%s]",veaf.p(itemType)) local name = item[2] veaf.loggers.get(veafRadio.Id):trace("name = [%s]",veaf.p(name)) if itemType == "menu" then -- this is a menu with a content local content = item[3] veaf.loggers.get(veafRadio.Id):trace("content = [%s]",veaf.p(content)) veaf.loggers.get(veafRadio.Id):trace("creating menu name=%s",veaf.p(name)) if groupId ~= nil then result = missionCommands.addSubMenuForGroup(groupId, name, parentMenu) else result = missionCommands.addSubMenu(name, parentMenu) end -- recurse if needed if content ~= nil and #content > 0 then _recursivelyCreateMenu(content, result) end else -- this is a command with a function local aFunction = item[3] veaf.loggers.get(veafRadio.Id):trace("aFunction = [%s]",veaf.p(aFunction)) local parameters = item[4] veaf.loggers.get(veafRadio.Id):trace("parameters = [%s]",veaf.p(parameters)) veaf.loggers.get(veafRadio.Id):trace("creating command name=%s",veaf.p(name)) if groupId ~= nil then missionCommands.addCommandForGroup(groupId, name, parentMenu, aFunction, parameters) else missionCommands.addCommand(name, parentMenu, aFunction, parameters) end end end end _recursivelyCreateMenu(configuration, nil) end -- helper functions for user menus local spawnCapFunction = function () end function veafRadio.menu(name, ...) return { "menu", name, {...} } end function veafRadio.command(name, aFunction, parameters) return { "command", name, aFunction, parameters } end function veafRadio.mainmenu(...) return { ... } end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafRadio.initialize(skipHelpMenus) -- Find the path of the SRS radio configuration script -- We're going to need it to define : -- STTS.DIRECTORY --- STTS.SRS_PORT local srsConfigPath=nil local l_lfs = lfs if not l_lfs and SERVER_CONFIG and SERVER_CONFIG.getModule then l_lfs = SERVER_CONFIG.getModule("lfs") end if l_lfs then srsConfigPath = l_lfs.writedir() .. "\\DCS-SimpleRadio-Standalone\\SRS_for_scripting_config.lua" veaf.loggers.get(veafRadio.Id):debug(string.format("srsConfigPath = %s", tostring(srsConfigPath))) --local test = l_lfs.currentdir() --veaf.loggers.get(veafRadio.Id):debug(string.format("test = %s", tostring(test))) if srsConfigPath then -- execute the script local file = loadfile(srsConfigPath) if file then file() veaf.loggers.get(veafRadio.Id):info("SRS configuration file loaded") if STTS then STTS.MP3_FOLDER = l_lfs.writedir() .."\\..\\..\\Music" veaf.loggers.get(veafRadio.Id):trace(string.format("STTS.SRS_PORT = %s", tostring(STTS.SRS_PORT))) veaf.loggers.get(veafRadio.Id):trace(string.format("STTS.DIRECTORY = %s", tostring(STTS.DIRECTORY))) veaf.loggers.get(veafRadio.Id):trace(string.format("STTS.EXECUTABLE = %s", tostring(STTS.EXECUTABLE))) end else veaf.loggers.get(veafRadio.Id):warn(string.format("Error while loading SRS configuration file [%s]",srsConfigPath)) end end end veafRadio.skipHelpMenus = skipHelpMenus or false -- Build the initial radio menu veafRadio.refreshRadioMenu(false) --mist.scheduleFunction(veafRadio._refreshRadioMenu,{},timer.getTime()+15) --TODO check if this is still needed (commented out when added the BIRTH event handler) -- add marker change event handler veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafRadio.onEventMarkChange) -- add human birth event handler veafEventHandler.addCallback("veafRadio.eventHandler", {"S_EVENT_BIRTH", "S_EVENT_PLAYER_ENTER_UNIT"}, veafRadio.onBirthEvent) -- start the beacons veafRadio._runBeacons() end veaf.loggers.get(veafRadio.Id):info(string.format("Loading version %s", veafRadio.Version)) ------------------ END script veafRadio.lua ------------------ ------------------ START script veafRemote.lua ------------------ ------------------------------------------------------------------ -- VEAF remote callback functions for DCS World -- By zip (2020) -- -- Features: -- --------- -- * This module offers support for calling script from a web server or a server hook -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafRemote = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafRemote.Id = "REMOTE" --- Version. veafRemote.Version = "2.3.0" -- trace level, specific to this module --veafRemote.LogLevel = "trace" veaf.loggers.new(veafRemote.Id, veafRemote.LogLevel) -- if false, SLMOD will not be called for regular commands veafRemote.USE_SLMOD = false -- if false, SLMOD will never be called veafRemote.USE_SLMOD_FOR_SPECIAL_COMMANDS = false veafRemote.CommandStarter = "_remote" veafRemote.MIN_LEVEL_FOR_MARKER = 10 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafRemote.monitoredCommands = {} veafRemote.remoteUsers = {} veafRemote.remoteUnitsPilots = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- NIOD callbacks ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafRemote.addNiodCallback(name, parameters, code) if niod then veaf.loggers.get(veafRemote.Id):info("Adding NIOD function "..name) niod.functions[name] = function(payload) -- start of inline function veaf.loggers.get(veafRemote.Id):debug(string.format("niod callback [%s] was called with payload %s", veaf.p(name), veaf.p(payload))) local errors = {} -- check mandatory parameters presence for parameterName, parameterData in pairs(parameters) do veaf.loggers.get(veafRemote.Id):trace(string.format("checking if parameter [%s] is mandatory", veaf.p(parameterName))) if parameterData and parameterData.mandatory then if not (payload and payload[parameterName]) then local text = "missing mandatory parameter "..parameterName veaf.loggers.get(veafRemote.Id):trace(text) table.insert(errors, text) end end end -- check parameters type if payload then for parameterName, value in pairs(payload) do local parameter = parameters[parameterName] if not parameter then table.insert(errors, "unknown parameter "..parameterName) elseif value and not(type(value) == parameter.type) then local text = string.format("parameter %s should have type %s, has %s ", parameterName, parameter.type, type(value)) veaf.loggers.get(veafRemote.Id):trace(text) table.insert(errors, text) end end end -- stop on error if #errors > 0 then local errorMessage = "" for _, error in pairs(errors) do errorMessage = errorMessage .. "\n" .. error end veaf.loggers.get(veafRemote.Id):error(string.format("niod callback [%s] was called with incorrect parameters :", veaf.p(name), errorMessage)) return errorMessage else veaf.loggers.get(veafRemote.Id):trace(string.format("payload = %s", veaf.p(payload))) veaf.loggers.get(veafRemote.Id):trace(string.format("unpacked payload = %s", veaf.p(veaf.safeUnpack(payload)))) local status, retval = pcall(code,veaf.safeUnpack(payload)) if status then return retval else return "an error occured : "..veaf.p(status) end end end -- of inline function else veaf.loggers.get(veafRemote.Id):error("NIOD is not loaded !") end end function veafRemote.addNiodCommand(name, command) veafRemote.addNiodCallback( name, { parameters={ mandatory=false, type="string"}, x={ mandatory=false, type="number"}, y={ mandatory=false, type="number"}, z={ mandatory=false, type="number"}, silent={ mandatory=false, type="boolean"} }, function(parameters, x, y, z, silent) veaf.loggers.get(veafRemote.Id):debug(string.format("niod->command %s (%s, %s, %s, %s, %s)", veaf.p(parameters), veaf.p(x), veaf.p(y), veaf.p(z), veaf.p(silent))) return veafRemote.executeCommand({x=x or 0, y=y or 0, z=z or 0}, command..parameters) end ) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- default endpoints list ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafRemote.buildDefaultList() local TEST = false if TEST then -- test veafRemote.addNiodCallback( "test", { param1S_M={ mandatory=true, type="string"}, param2S={ mandatory=false, type="string"}, param3N={ mandatory=false, type="number"}, param4B={ mandatory=false, type="boolean"}, }, function(param1S_M, param2S, param3N, param4B) local text = string.format("niod.test(%s, %s, %s, %s)", veaf.p(param1S_M), veaf.p(param2S), veaf.p(param3N), veaf.p(param4B)) veaf.loggers.get(veafRemote.Id):debug(text) trigger.action.outText(text, 15) end ) -- login veafRemote.addNiodCallback( "login", { password={ mandatory=true, type="string"}, timeout={ mandatory=false, type="number"}, silent={ mandatory=false, type="boolean"} }, function(password, timeout, silent) veaf.loggers.get(veafRemote.Id):debug(string.format("niod.login(%s, %s, %s)",veaf.p(password), veaf.p(timeout),veaf.p(silent))) -- TODO remove password from log if veafSecurity.checkPassword_L1(password) then veafSecurity.authenticate(timeout) return "Mission is unlocked" else return "wrong password" end end ) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function executed when a mark has changed. This happens when text is entered or changed. function veafRemote.onEventMarkChange(eventPos, event) if veafRemote.executeCommand(eventPos, event.text) then -- Delete old mark. veaf.loggers.get(veafRemote.Id):trace(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafRemote.executeCommand(eventPos, eventText) veaf.loggers.get(veafRemote.Id):debug(string.format("veafRemote.executeCommand(eventText=[%s])", tostring(eventText))) -- Check if marker has a text and the veafRemote.CommandStarter keyphrase. if eventText ~= nil and eventText:lower():find(veafRemote.CommandStarter) then -- Analyse the mark point text and extract the keywords. local command, password = veafRemote.markTextAnalysis(eventText) if command then -- do the magic return veafRemote.executeRemoteCommand(command, password) end end end --- Extract keywords from mark text. function veafRemote.markTextAnalysis(text) veaf.loggers.get(veafRemote.Id):trace(string.format("veafRemote.markTextAnalysis(text=[%s])", tostring(text))) if text then -- extract command and password local password, command = text:match(veafRemote.CommandStarter.."#?([^%s]*)%s+(.+)") if command then veaf.loggers.get(veafRemote.Id):trace(string.format("command = [%s]", command)) return command, password end end return nil end -- execute a command function veafRemote.executeRemoteCommand(command, password) local command = command or "" local password = password or "" veaf.loggers.get(veafRemote.Id):debug(string.format("veafRemote.executeRemoteCommand([%s])",command)) if not(veafSecurity.checkPassword_L1(password)) then veaf.loggers.get(veafRemote.Id):error(string.format("veafRemote.executeRemoteCommand([%s]) - bad or missing password",command)) trigger.action.outText("Bad or missing password",5) return false end local commandData = veafRemote.monitoredCommands[command:lower()] if commandData then local scriptToExecute = commandData.script veaf.loggers.get(veafRemote.Id):trace(string.format("found script [%s] for command [%s]", scriptToExecute, command)) local authorized = (not(commandData.requireAdmin)) or (veafSecurity.checkSecurity_L9(password)) if not authorized then return false else local result, err = mist.utils.dostring(scriptToExecute) if result then veaf.loggers.get(veafRemote.Id):debug(string.format("veafRemote.executeRemoteCommand() - lua code was successfully called for script [%s]", scriptToExecute)) return true else veaf.loggers.get(veafRemote.Id):error(string.format("veafRemote.executeRemoteCommand() - error [%s] calling lua code for script [%s]", err, scriptToExecute)) return false end end else veaf.loggers.get(veafRemote.Id):warn(string.format("veafRemote.executeRemoteCommand : cannot find command [%s]",command or "")) end return false end -- execute command from the remote interface (see VEAF-server-hook.lua) function veafRemote.executeCommandFromRemote(username, level, unitName, veafModule, command) veaf.loggers.get(veafRemote.Id):debug(string.format("veafRemote.executeCommandFromRemote([%s], [%s], [%s], [%s], [%s])", veaf.p(username), veaf.p(level), veaf.p(unitName), veaf.p(veafModule), veaf.p(command))) --local _user = veafRemote.getRemoteUser(username) --veaf.loggers.get(veafRemote.Id):trace(string.format("_user = [%s]",veaf.p(_user))) --if not _user then -- return false --end if not veafModule or not username or not command then return false end local _user = { name = username, level = tonumber(level or "-1")} local _parameters = { _user, username, unitName, command } local _status, _retval local _module = veafModule:lower() if _module == "air" then veaf.loggers.get(veafRemote.Id):debug(string.format("running veafCombatMission.executeCommandFromRemote")) _status, _retval = pcall(veafCombatMission.executeCommandFromRemote, _parameters) elseif _module == "point" then veaf.loggers.get(veafRemote.Id):debug(string.format("running veafNamedPoints.executeCommandFromRemote")) _status, _retval = pcall(veafNamedPoints.executeCommandFromRemote, _parameters) elseif _module == "atis" or _module == "atc" or _module == "weather" then veaf.loggers.get(veafRemote.Id):debug(string.format("running veafWeather.executeCommandFromRemote")) _status, _retval = pcall(veafWeather.executeCommandFromRemote, _parameters) elseif _module == "alias" then veaf.loggers.get(veafRemote.Id):debug(string.format("running veafShortcuts.executeCommandFromRemote")) _status, _retval = pcall(veafShortcuts.executeCommandFromRemote, _parameters) elseif _module == "carrier" then veaf.loggers.get(veafRemote.Id):debug(string.format("running veafShortcuts.executeCommandFromRemote")) _status, _retval = pcall(veafCarrierOperations.executeCommandFromRemote, _parameters) elseif _module == "secu" then veaf.loggers.get(veafRemote.Id):debug(string.format("running veafSecurity.executeCommandFromRemote")) _status, _retval = pcall(veafSecurity.executeCommandFromRemote, _parameters) else veaf.loggers.get(veafRemote.Id):error(string.format("Module not found : [%s]", veaf.p(veafModule))) return false end veaf.loggers.get(veafRemote.Id):trace(string.format("_status = [%s]",veaf.p(_status))) veaf.loggers.get(veafRemote.Id):trace(string.format("_retval = [%s]",veaf.p(_retval))) if not _status then veaf.loggers.get(veafRemote.Id):error(string.format("Error when [%s] tried running [%s] in module [%s]; it returned %s", veaf.p(_user.name), veaf.p(_parameters), veaf.p(veafModule), veaf.p(_retval))) else veaf.loggers.get(veafRemote.Id):info(string.format("[%s] ran [%s] in module [%s]; it returned %s", veaf.p(_user.name), veaf.p(_parameters), veaf.p(veafModule), veaf.p(_retval))) end return _status end -- register a user from the server function veafRemote.registerUser(username, userpower, ucid) veaf.loggers.get(veafRemote.Id):debug(string.format("veafRemote.registerUser([%s], [%s], [%s])",veaf.p(username), veaf.p(userpower), veaf.p(ucid))) if not username or not ucid then return false end veafRemote.remoteUsers[username:lower()] = { name = username, level = tonumber(userpower or "-1"), ucid = ucid } end -- register a user slot from the server; called when the player changes slot function veafRemote.registerUserSlot(username, ucid, unitName) veaf.loggers.get(veafRemote.Id):debug(string.format("veafRemote.registerUserSlot([%s], [%s], [%s])",veaf.p(username), veaf.p(ucid), veaf.p(unitName))) if not username or not unitName then return false end local remoteUser = veafRemote.remoteUsers[username:lower()] if not remoteUser then remoteUser = { name = username, ucid = ucid} end local previousUnit = remoteUser.unitName remoteUser.unitName = unitName -- can be nil if the player got out of the unit -- unregister the previous unit, if any if previousUnit then veafRemote.remoteUnitsPilots[previousUnit] = nil end -- register the current unit, if any if unitName then veafRemote.remoteUnitsPilots[unitName] = remoteUser end end -- return a user from the server table function veafRemote.getRemoteUser(username) veaf.loggers.get(veafRemote.Id):debug(string.format("veafRemote.getRemoteUser([%s])",veaf.p(username))) veaf.loggers.get(veafRemote.Id):trace(string.format("veafRemote.remoteUsers = [%s]",veaf.p(veafRemote.remoteUsers))) if not username then return nil end return veafRemote.remoteUsers[username:lower()] end -- return a user from the server units table function veafRemote.getRemoteUserFromUnit(unitName) veaf.loggers.get(veafRemote.Id):debug(string.format("veafRemote.getRemoteUserFromUnit([%s])",veaf.p(unitName))) veaf.loggers.get(veafRemote.Id):trace(string.format("veafRemote.remoteUnitsPilots = [%s]",veaf.p(veafRemote.remoteUnitsPilots))) if not unitName then return nil end return veafRemote.remoteUnitsPilots[unitName] end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafRemote.initialize() veaf.loggers.get(veafRemote.Id):info("Initializing module") veafRemote.buildDefaultList() veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafRemote.onEventMarkChange) end veaf.loggers.get(veafRemote.Id):info(string.format("Loading version %s", veafRemote.Version)) ------------------ END script veafRemote.lua ------------------ ------------------ START script veafSpawn.lua ------------------ ------------------------------------------------------------------ -- VEAF spawn command and functions for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Listen to marker change events and execute spawn commands, with optional parameters -- * Possibilities : -- * - spawn a specific ennemy unit or group -- * - create a cargo drop to be picked by a helo -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ --- veafSpawn Table. veafSpawn = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafSpawn.Id = "SPAWN" --- Version. veafSpawn.Version = "1.59.0" -- trace level, specific to this module --veafSpawn.LogLevel = "trace" veaf.loggers.new(veafSpawn.Id, veafSpawn.LogLevel) --- Key phrase to look for in the mark text which triggers the spawn command. veafSpawn.SpawnKeyphrase = "_spawn" --- Key phrase to look for in the mark text which triggers the destroy command. veafSpawn.DestroyKeyphrase = "_destroy" --- Key phrase to look for in the mark text which triggers the teleport command. veafSpawn.TeleportKeyphrase = "_teleport" --- Key phrase to look for in the mark text which triggers the drawing commands. veafSpawn.DrawingKeyphrase = "_drawing" --- Key phrase to look for in the mark text which triggers the mission master commands. veafSpawn.MissionMasterKeyphrase = "_mm" --- Illumination flare default initial altitude (in meters AGL) veafSpawn.IlluminationFlareAglAltitude = 1000 veafSpawn.RadioMenuName = "SPAWN" veafSpawn.HideRadioMenu = false --- static object type spawned when using the "logistic" keyword veafSpawn.LogisticUnitType = "FARP Ammo Dump Coating" veafSpawn.LogisticUnitCategory = "Fortifications" veafSpawn.ShellingInterval = 5 -- seconds between shells, randomized by 30% veafSpawn.FlakingInterval = 2 -- seconds between flak shells, randomized by 30% veafSpawn.IlluminationShellingInterval = 45 -- seconds between illumination shells, randomized by 30% veafSpawn.MIN_REPEAT_DELAY = 5 veafSpawn.HoundElintAddDelay = 1 --delay before attempting to add a unit to Hound Elint, required for aircrafts spawned dynamically at least veafSpawn.AirUnitTemplatesPrefix = "veafSpawn-" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafSpawn.rootPath = nil -- counts the units generated veafSpawn.spawnedUnitsCounter = 0 -- store all the convoys spawned veafSpawn.spawnedConvoys = {} -- store all the air units templates (groups, actually) veafSpawn.airUnitTemplates = {} -- all the named groups that have been spawned veafSpawn.spawnedNamesIndex = {} -- time delay between the watchdog checks for each CAP veafSpawn.CAP_WATCHDOG_DELAY = 10 -- range scale of cargo weight biases veafSpawn.cargoWeightBiasRange = 6 --AFAC related base data veafSpawn.AFAC = {} -- number of AFAC spawned veafSpawn.AFAC.numberSpawned = {} veafSpawn.AFAC.numberSpawned[coalition.side.BLUE] = nil veafSpawn.AFAC.numberSpawned[coalition.side.RED] = nil -- maximum number of AFACs allowed for spawning by players veafSpawn.AFAC.maximumAmount = 8 -- base frequency for the first AFAC spawned veafSpawn.AFAC.baseAFACfrequency = {} veafSpawn.AFAC.baseAFACfrequency[coalition.side.BLUE] = 226300000 -- 226.300000 MHz otherwise known as 226300000 Hz veafSpawn.AFAC.baseAFACfrequency[coalition.side.RED] = 226300000 -- 226.300000 MHz otherwise known as 226300000 Hz -- callsign list of the AFACs veafSpawn.AFAC.callsigns = {} veafSpawn.AFAC.callsigns[coalition.side.BLUE] = { [1] = {name = "Enfield 9 1", taken = false}, [2] = {name = "Springfield 9 1", taken = false}, [3] = {name = "Uzi 9 1", taken = false}, [4] = {name = "Colt 9 1", taken = false}, [5] = {name = "Dodge 9 1", taken = false}, [6] = {name = "Ford 9 1", taken = false}, [7] = {name = "Chevy 9 1", taken = false}, [8] = {name = "Pontiac 9 1", taken = false}, } veafSpawn.AFAC.callsigns[coalition.side.RED] = { [1] = {name = "181", taken = false}, [2] = {name = "281", taken = false}, [3] = {name = "381", taken = false}, [4] = {name = "481", taken = false}, [5] = {name = "581", taken = false}, [6] = {name = "681", taken = false}, [7] = {name = "781", taken = false}, [8] = {name = "881", taken = false}, } -- AFAC mission data as MIST isn't able to recover it from dynamically spawned aircrafts veafSpawn.AFAC.missionData = {} veafSpawn.AFAC.missionData[coalition.side.BLUE] = {} veafSpawn.AFAC.missionData[coalition.side.RED] = {} veafSpawn.traceMarkerId = 3727 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function executed when a mark has changed. This happens when text is entered or changed. function veafSpawn.onEventMarkChange(eventPos, event) veaf.loggers.get(veafSpawn.Id):trace(string.format("event = %s", veaf.p(event))) -- choose by default the coalition opposing the player who triggered the event local invertedCoalition = 1 if event.coalition == 1 then invertedCoalition = 2 end veaf.loggers.get(veafSpawn.Id):trace(string.format("event.idx = %s", veaf.p(event.idx))) if veafSpawn.executeCommand(eventPos, event.text, invertedCoalition, event.idx, nil, nil, nil, nil, nil, true) then -- Delete old mark. veaf.loggers.get(veafSpawn.Id):trace(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafSpawn.executeCommand(eventPos, eventText, coalition, markId, bypassSecurity, spawnedGroups, repeatCount, repeatDelay, route, allowStartDelay) veaf.loggers.get(veafSpawn.Id):trace("eventPos=%s", eventPos) veaf.loggers.get(veafSpawn.Id):debug("eventText=%s", eventText) veaf.loggers.get(veafSpawn.Id):trace("coalition=%s", coalition) veaf.loggers.get(veafSpawn.Id):trace("markId=%s", markId) veaf.loggers.get(veafSpawn.Id):trace("bypassSecurity=%s", bypassSecurity) veaf.loggers.get(veafSpawn.Id):trace("repeatCount=%s", repeatCount) veaf.loggers.get(veafSpawn.Id):trace("repeatDelay=%s", repeatDelay) veaf.loggers.get(veafSpawn.Id):trace("route=%s", route) veaf.loggers.get(veafSpawn.Id):trace("allowStartDelay=%s", allowStartDelay) -- Check if marker has a text and the veafSpawn.SpawnKeyphrase keyphrase. if eventText ~= nil and (eventText:lower():find(veafSpawn.SpawnKeyphrase) or eventText:lower():find(veafSpawn.DestroyKeyphrase) or eventText:lower():find(veafSpawn.TeleportKeyphrase) or eventText:lower():find(veafSpawn.DrawingKeyphrase) or eventText:lower():find(veafSpawn.MissionMasterKeyphrase)) then -- Analyse the mark point text and extract the keywords. local options = veafSpawn.markTextAnalysis(eventText) if options then local repeatDelay = repeatDelay local repeatCount = repeatCount local allowStartDelay = allowStartDelay or false local startDelay = options.delayedStart if allowStartDelay and startDelay and startDelay > 0 then veaf.loggers.get(veafSpawn.Id):trace(string.format("scheduling veafSpawn.executeCommand for a delayed start in %s seconds", veaf.p(startDelay))) mist.scheduleFunction(veafSpawn.executeCommand, {eventPos, eventText, coalition, markId, bypassSecurity, spawnedGroups, nil, nil, route, false}, timer.getTime() + startDelay) return true end if options.repeatCount and not repeatCount then -- only use the parsed repeat options IF the parameter is not set (not during a repeat loop) -- set repeatCount and repeatDelay using the parsed options repeatCount = options.repeatCount repeatDelay = options.repeatDelay or veafSpawn.MIN_REPEAT_DELAY veaf.loggers.get(veafSpawn.Id):trace(string.format("using parsed repeat options to set repeatCount to %s and repeatDelay to %s", veaf.p(repeatCount), veaf.p(repeatDelay))) end if repeatCount and repeatCount > 0 then repeatDelay = repeatDelay if repeatDelay < veafSpawn.MIN_REPEAT_DELAY then repeatDelay = veafSpawn.MIN_REPEAT_DELAY end repeatCount = repeatCount - 1 -- schedule the next step of the repeated command veaf.loggers.get(veafSpawn.Id):trace(string.format("scheduling veafSpawn.executeCommand for %s repeats in %s seconds", veaf.p(repeatCount), veaf.p(repeatDelay))) mist.scheduleFunction(veafSpawn.executeCommand, {eventPos, eventText, coalition, markId, bypassSecurity, spawnedGroups, repeatCount, repeatDelay, route, false}, timer.getTime() + repeatDelay) end if not(options.radius) then if options.farp or options.cargo or options.logistic or options.destroy or options.teleport or options.bomb or options.smoke or options.flare or options.signal then options.radius = 0 else options.radius = 150 end end for i=1,options.multiplier do local spawnedGroup = nil if not options.side then if options.country then -- deduct the side from the country options.side = veaf.getCoalitionForCountry(options.country, true) else options.side = coalition end end if not options.country then -- deduct the country from the side options.country = veaf.getCountryForCoalition(options.side) end veaf.loggers.get(veafSpawn.Id):trace(string.format("options.side=%s",tostring(options.side))) veaf.loggers.get(veafSpawn.Id):trace(string.format("options.country=%s",tostring(options.country))) local routeDone = false --indication is the spawn is meant to be a convoy, to adapt it's spawning pattern local hasDest = false if (options.destination ~= nil) then hasDest = true end -- Check options commands if options.unit then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end local code = options.laserCode local channel = options.freq local band = options.mod if options.role == "tacan" then channel = options.tacanChannel or 99 code = options.tacanCode or ("T"..tostring(channel)) band = options.tacanBand or "X" end spawnedGroup = veafSpawn.spawnUnit(eventPos, options.radius, options.name, options.czName, options.country, options.altitude, options.heading, options.unitName, options.role, options.forceStatic, code, channel, band, bypassSecurity, not options.showMFD) elseif options.farp then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end if not options.type then options.type = "invisible" end local channel = options.tacanChannel local code = options.tacanCode local mod = options.tacanBand spawnedGroup = veafSpawn.spawnFarp(eventPos, options.radius, options.name, options.country, options.type, options.side, options.heading, options.spacing, bypassSecurity, not options.showMFD, options.noFarpMarkers, code, channel, mod) elseif options.fob then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnFob(eventPos, options.radius, options.name, options.country, options.type, options.side, options.heading, options.spacing, bypassSecurity, not options.showMFD) elseif options.cap then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnCombatAirPatrol(eventPos, options.radius, options.name, options.country, options.altitude, options.altitudedelta, options.heading, options.distance, options.speed, options.capradius, options.skill, bypassSecurity, options.showMFD) elseif options.afac then --check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnAFAC(eventPos, options.name, options.country, options.altitude, options.speed, options.heading, options.freq, options.mod, options.laserCode, options.immortal, false, options.showMFD) elseif options.group then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnGroup(eventPos, options.radius, options.name, options.czName, options.country, options.altitude, options.heading, options.spacing, options.unitName, bypassSecurity, hasDest, not options.showMFD) elseif options.infantryGroup then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnInfantryGroup(eventPos, options.radius, options.czName, options.country, options.side, options.heading, options.spacing, options.defense, options.armor, options.size, bypassSecurity, not options.showMFD) elseif options.armoredPlatoon then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnArmoredPlatoon(eventPos, options.radius, options.czName, options.country, options.side, options.heading, options.spacing, options.defense, options.armor, options.size, bypassSecurity, hasDest, not options.showMFD) elseif options.airDefenseBattery then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnAirDefenseBattery(eventPos, options.radius, options.czName, options.country, options.side, options.heading, options.spacing, options.defense, bypassSecurity, hasDest, not options.showMFD) elseif options.transportCompany then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnTransportCompany(eventPos, options.radius, options.czName, options.country, options.side, options.heading, options.spacing, options.defense, options.size, bypassSecurity, hasDest, not options.showMFD) elseif options.fullCombatGroup then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnFullCombatGroup(eventPos, options.radius, options.czName, options.country, options.side, options.heading, options.spacing, options.defense, options.armor, options.size, bypassSecurity, not options.showMFD) elseif options.convoy then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnConvoy(eventPos, options.name, options.czName, options.radius, options.country, options.side, options.heading, options.spacing, options.speed, options.patrol, options.offroad, options.destination, options.defense, options.size, options.armor, bypassSecurity, not options.showMFD) routeDone = true elseif options.cargo then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnCargo(eventPos, options.radius, options.cargoType, options.country, options.cargoWeightBias, options.cargoSmoke, options.unitName, bypassSecurity, not options.showMFD) elseif options.logistic then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end spawnedGroup = veafSpawn.spawnLogistic(eventPos, options.radius, options.country, bypassSecurity, not options.showMFD) elseif options.destroy then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end veafSpawn.destroy(eventPos, options.radius, options.unitName) elseif options.teleport then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end veafSpawn.teleport(eventPos, options.name, bypassSecurity) elseif options.bomb then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end veafSpawn.spawnBomb(eventPos, options.radius, options.shells, options.power, options.altitude, options.altitudedelta, options.password) elseif options.smoke then veafSpawn.spawnSmoke(eventPos, options.smokeColor, options.radius, options.shells) elseif options.flare then if not options.altitude or options.altitude == 0 then options.altitude = 1000 end if not options.power or options.power == 0 then options.power = 500 end options.power = options.power * 1000 veafSpawn.spawnIlluminationFlare(eventPos, options.radius, options.shells, options.power, options.altitude, options.heading, options.distance, options.speed) elseif options.signal then veafSpawn.spawnSignalFlare(eventPos, options.radius, options.shells, options.smokeColor) elseif options.addDrawing then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end veafSpawn.addPointToDrawing(eventPos, options.name, options.drawColor, options.drawFillColor, options.type, options.drawArrow) elseif options.drawSquare then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end veafSpawn.drawSquare(eventPos, options.name, options.radius, options.drawColor, options.drawFillColor, options.type) elseif options.drawCircle then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end veafSpawn.drawCircle(eventPos, options.name, options.radius, options.drawColor, options.drawFillColor, options.type) elseif options.eraseDrawing then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_L1(options.password, markId)) then return end veafSpawn.eraseDrawing(options.name) elseif options.mmFlagOn then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_MM(options.password)) then return end veafSpawn.missionMasterSetFlag(options.name, 1) elseif options.mmFlagOff then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_MM(options.password)) then return end veafSpawn.missionMasterSetFlag(options.name, 0) elseif options.mmGetFlag then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_MM(options.password)) then return end veafSpawn.missionMasterGetFlag(options.name) elseif options.mmRun then -- check security if not (bypassSecurity or veafSecurity.checkSecurity_MM(options.password)) then return end veafSpawn.missionMasterRun(options.name) end if spawnedGroup then local groupObject = Group.getByName(spawnedGroup) local isStatic = false --group might not have been found because it was a static if not groupObject then isStatic = true groupObject = StaticObject.getByName(spawnedGroup) end veaf.loggers.get(veafSpawn.Id):trace("got groupObject (isStatic=%s) to add group to other platforms : %s", veaf.p(isStatic), veaf.p(groupObject)) if groupObject then if not isStatic then --stuff below does not support statics -- make the group combat ready ! well except if the user said otherwise, tweak the AlarmState for some scenarios --veaf.loggers.get(veafSpawn.Id):trace("options.disperse=%s", veaf.p(options.disperse)) veaf.readyForCombat(groupObject, options.AlarmState, options.disperse) if not route and not routeDone and options.destination then -- make the group go to destination local actualPosition = groupObject:getUnit(1):getPosition().p local route = veaf.generateVehiclesRoute(actualPosition, options.destination, not options.offroad, options.speed, options.patrol, spawnedGroup) mist.goRoute(groupObject, route) elseif route then mist.goRoute(groupObject, route) end -- add the group to the IADS, if there is one if veafSkynet and not veafSkynet.DynamicSpawn and options.skynet then -- only add static stuff like sam groups and sam batteries, not mobile groups and convoys -- and do not do that if DynamicSpawn is active in VeafSkynet veaf.loggers.get(veafSpawn.Id):trace("options.skynet= %s", veaf.p(options.skynet)) if type(options.skynet) == "boolean" then --it means options.skynet is true options.skynet = veafSkynet.defaultIADS[tostring(options.side)] end veaf.loggers.get(veafSpawn.Id):trace("Adding spawned group to skynet, networkName= %s", veaf.p(options.skynet)) local networkName = options.skynet if veafSkynet.addGroupToNetwork(networkName, groupObject, options.forceEwr, options.pointDefense, nil, bypassSecurity) then veaf.loggers.get(veafSpawn.Id):trace("Group Added to IADS network") if not bypassSecurity then trigger.action.outText(string.format("Group added to the IADS named \"%s\"", options.skynet),15) end else veaf.loggers.get(veafSpawn.Id):trace("Could not find IADS network or group is not supported by IADS") if not bypassSecurity then trigger.action.outText(string.format("Could not add group to the IADS named \"%s\", network not found or group not supported", options.skynet),15) end end end end --but houndElint for example does support statics -- reset the Hound Elint system, if the module is active if veafHoundElint then mist.scheduleFunction(veafHoundElint.addPlatformToSystem, {groupObject, nil, false}, timer.getTime()+veafSpawn.HoundElintAddDelay) end --might need to specify the if a group was static in here so that people on the other end know if spawnedGroups then table.insert(spawnedGroups, spawnedGroup) end end end end return true end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Analyse the mark text and extract keywords. ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafSpawn.convertLaserToFreq(laser) veaf.loggers.get(veafSpawn.Id):trace(string.format("convertLaserToFreq(laser=%s)", tostring(laser))) local laser = tonumber(laser) if laser and laser >= 1111 and laser <= 1688 then local laserB = math.floor((laser - 1000)/100) local laserCD = laser - 1000 - laserB*100 local frequency = tostring(30+laserB+laserCD*0.05) veaf.loggers.get(veafSpawn.Id):trace(string.format("laserB=%s", tostring(laserB))) veaf.loggers.get(veafSpawn.Id):trace(string.format("laserCD=%s", tostring(laserCD))) veaf.loggers.get(veafSpawn.Id):trace(string.format("frequency=%s", tostring(frequency))) return frequency else return nil end end --- Extract keywords from mark text. function veafSpawn.markTextAnalysis(text) veaf.loggers.get(veafSpawn.Id):trace(string.format("veafSpawn.markTextAnalysis(text=%s)", text)) -- Option parameters extracted from the mark text. local options = {} options.czName = nil -- name of the CZ to add to the group name, if any options.unit = false options.forceStatic = false -- if true, will force the spawned unit to be a static options.group = false options.cap = false options.farp = false options.noFarpMarkers = false options.fob = false options.type = nil options.cargo = false options.logistic = false options.smoke = false options.flare = false options.signal = false options.bomb = false options.destroy = false options.teleport = false options.convoy = false options.role = nil options.laserCode = 1688 options.infantryGroup = false options.armoredPlatoon = false options.airDefenseBattery = false options.transportCompany = false options.fullCombatGroup = false options.speed = nil options.capradius = nil options.shells = 1 options.multiplier = 1 options.skynet = false -- if true, add to skynet options.forceEwr = false -- if true, unit will be added as an IADS EWR options.pointDefense = false -- if true, unit will be added as point defense to the closest IADS SAM site options.AlarmState = 2 -- Alarm state of the convoy to be spawned, 0 is AUTO, 1 is GREEN, 2 is RED. Note: This option is useful for some vehicules which behave badly in Alarm State RED when spawned such as the Scud or Sa-11 (they deploy and can't drive anywhere). Auto is better suited options.disperse = 15 --disperse time of groups if under attack, by default is set to 20s options.showMFD = false --option to enable groups to be seen on MFDs options.addDrawing = false -- draw a polygon on the map options.drawSquare = false -- draw a square on the map options.drawCircle = false -- draw a circle on the map options.eraseDrawing = false -- erase a polygon from the map options.stopDrawing = false -- close a polygon started on the map options.drawColor = nil options.drawFillColor = nil options.drawArrow = nil -- spawned group/unit type/alias options.name = "" -- spawned unit name options.unitName = nil -- spawned group units spacing options.spacing = 5 options.country = nil options.side = nil options.altitude = 0 options.altitudedelta = 0 options.heading = 0 options.distance = nil options.skill = nil -- if true, group is part of a road convoy options.isConvoy = false -- if true, group is patroling between its spawn point and its destination named point options.patrol = false -- if true, group is set to not follow roads options.offroad = false -- if set and convoy is true, send the group to the named point options.destination = nil -- the size of the generated dynamic groups (platoons, convoys, etc.) options.size = math.random(7) + 8 -- defenses force ; ranges from 1 to 5, 5 being the toughest. options.defense = math.random(5) -- armor force ; ranges from 1 to 5, 5 being the strongest and most modern. options.armor = math.random(5) -- bomb power options.power = 100 -- smoke color options.smokeColor = trigger.smokeColor.Red -- optional cargo smoke options.cargoSmoke = false -- cargo type options.cargoType = "container_cargo" options.cargoWeightBias = 2 --weight bias of the cargo, if equal to 0, cargo will be very close to minimum weight, if equal to 5, cargo will be close to maximum options.password = nil --AFAC spawn option options.afac = false options.immortal = false -- JTAC radio comms options.freq = veafSpawn.convertLaserToFreq(options.laserCode) options.mod = "fm" -- TACAN name and channel options.tacanChannel = nil options.tacanBand = nil -- repeat options options.repeatCount = nil options.repeatDelay = nil -- delayed start option options.delayedStart = 0 -- Check for correct keywords. if text:lower():find(veafSpawn.SpawnKeyphrase .. " unit") then options.unit = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " afac") then options.afac = true --default country for the AFAC options.country = "USA" --default AFAC spawned options.name = "mq-9" elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " cap") then options.cap = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " group") then options.group = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " farp") then options.farp = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " fob") then options.fob = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " convoy") then options.convoy = true options.size = 10 -- default the size parameter to 10 elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " infantrygroup") then options.infantryGroup = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " armorgroup") then options.armoredPlatoon = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " samgroup") then options.airDefenseBattery = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " transportgroup") then options.transportCompany = true options.size = math.random(2, 5) elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " combatgroup") then options.fullCombatGroup = true options.size = 1 -- default the size parameter to 1 elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " smoke") then options.smoke = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " flare") then options.flare = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " signal") then options.signal = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " cargo") then options.cargo = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " logistic") then options.logistic = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " bomb") then options.bomb = true elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " jtac") then options.role = 'jtac' options.unit = true -- default country for friendly JTAC: USA options.country = "USA" -- default name for JTAC options.name = "LUV HMMWV Jeep" -- default JTAC name (will overwrite previous unit with same name) options.unitName = "JTAC1" elseif text:lower():find(veafSpawn.SpawnKeyphrase .. " tacan") then options.role = 'tacan' options.unit = true -- default country for friendly tacan: USA options.country = "USA" -- default name for tacan options.name = "TACAN_beacon" -- default name (will overwrite previous unit with same name) options.unitName = "TACAN TCN" elseif text:lower():find(veafSpawn.DestroyKeyphrase) then options.destroy = true elseif text:lower():find(veafSpawn.TeleportKeyphrase) then options.teleport = true elseif text:lower():find(veafSpawn.DrawingKeyphrase .. " add") then options.addDrawing = true elseif text:lower():find(veafSpawn.DrawingKeyphrase .. " erase") then options.eraseDrawing = true elseif text:lower():find(veafSpawn.DrawingKeyphrase .. " square") then options.drawSquare = true elseif text:lower():find(veafSpawn.DrawingKeyphrase .. " circle") then options.drawCircle = true elseif text:lower():find(veafSpawn.MissionMasterKeyphrase .. " flagon") then options.mmFlagOn = true elseif text:lower():find(veafSpawn.MissionMasterKeyphrase .. " flagoff") then options.mmFlagOff = true elseif text:lower():find(veafSpawn.MissionMasterKeyphrase .. " getflag") then options.mmGetFlag = true elseif text:lower():find(veafSpawn.MissionMasterKeyphrase .. " run") then options.mmRun = true else return nil end -- keywords are split by "," local keywords = veaf.split(text, ",") for _, keyphrase in pairs(keywords) do -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. local str = veaf.breakString(veaf.trim(keyphrase), " ") local key = str[1] local val = str[2] or "" if key:lower() == "unitname" then -- Set name. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword unitname = %s", tostring(val))) options.unitName = val end if key:lower() == "name" then -- Set name. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword name = %s", tostring(val))) options.name = val end if key:lower() == "czname" then -- Set name. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword czname = %s", tostring(val))) options.czName = val end if (key:lower() == "destination" or key:lower() == "dest") then -- Set destination. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword destination = %s", tostring(val))) options.destination = val options.AlarmState = 0 --since some units will not move when they are told to have an alarm state red, it's best to by default leave it on auto. AI is pretty all knowing anyways, it knows when it should go to red state options.spacing = 1 --compress the convoy to not make it extremely long at departure options.radius = 1 --convoy spawns on the marker exactly to not have them spawn in trees etc. end if key:lower() == "isconvoy" then veaf.loggers.get(veafSpawn.Id):trace("Keyword isconvoy found") options.convoy = true end if key:lower() == "patrol" then veaf.loggers.get(veafSpawn.Id):trace("Keyword patrol found") options.patrol = true end if key:lower() == "offroad" then veaf.loggers.get(veafSpawn.Id):trace("Keyword offroad found") options.offroad = true end if key:lower() == "skynet" then -- Retreive the name of the IADS you wish to add the spawned group to veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword skynet = %s", tostring(val))) options.skynet = val:lower() if options.skynet == "" or options.skynet == "true" then options.skynet = true elseif options.skynet == "false" then options.skynet = false end end if key:lower() == "ewr" then -- Set force IADS EWR toggle for unit spawn veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword ewr found")) options.forceEwr = true end if key:lower() == "pointdefense" then -- Tells IADS to add the spawned SAM to the point defenses of the specified site or to the nearest site veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword pointdefense found")) options.pointDefense = true if val ~= "" then veaf.loggers.get(veafSpawn.Id):trace(string.format("groupName specified : %s", tostring(val))) options.pointDefense = tostring(val) end end --to be placed after the skynet input, SAMs in the skynet network work better if set to AlarmState RED, so AlarmState is equal to 2 if skynet is enabled if key:lower() == "alarm" then -- Set Alarm State of the unit to be spawned veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword alarm = %s", tostring(val))) if (val == "0" or val == "2" or val =="1") and not options.skynet then options.AlarmState = tonumber(val) end end if key:lower() == "radius" then -- Set name. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword radius = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.radius = nVal end if key:lower() == "spacing" then -- Set spacing. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword spacing = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.spacing = nVal end if key:lower() == "multiplier" then -- Set multiplier. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword multiplier = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.multiplier = nVal end if key:lower() == "alt" then -- Set altitude. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword alt = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.altitude = nVal end if key:lower() == "altdelta" then -- Set altitude delta. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword altdelta = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.altitudedelta = nVal end if key:lower() == "speed" then -- Set speed. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword speed = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.speed = nVal end if key:lower() == "capradius" then -- Set capradius. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword capradius = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.capradius = nVal end if key:lower() == "shells" then -- Set altitude. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword shells = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.shells = nVal end if key:lower() == "hdg" then -- Set heading. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword hdg = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.heading = nVal end if key:lower() == "heading" then -- Set heading. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword heading = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.heading = nVal end if key:lower() == "country" then -- Set country veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword country = %s", tostring(val))) options.country = val:upper() end if key:lower() == "side" then -- Set side veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword side = %s", tostring(val))) if val:upper() == "BLUE" then options.side = veafCasMission.SIDE_BLUE else options.side = veafCasMission.SIDE_RED end end if key:lower() == "password" then -- Unlock the command veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword password", tostring(val))) options.password = val end if key:lower() == "power" then -- Set bomb power. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword power = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.power = nVal end if key:lower() == "laser" then -- Set laser code. veaf.loggers.get(veafSpawn.Id):trace(string.format("laser code = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.freq = veafSpawn.convertLaserToFreq(nVal) options.laserCode = nVal end if key:lower() == "freq" then -- Set JTAC/AFAC frequency. veaf.loggers.get(veafSpawn.Id):trace(string.format("freq = %s", tostring(val))) options.freq = val end if key:lower() == "mod" then -- Set JTAC/AFAC modulation. veaf.loggers.get(veafSpawn.Id):trace(string.format("mod = %s", tostring(val))) options.mod = val end if key:lower() == "band" then -- Set TACAN band veaf.loggers.get(veafSpawn.Id):trace(string.format("band = %s", tostring(val))) options.tacanBand = val end if key:lower() == "code" then -- Set TACAN code veaf.loggers.get(veafSpawn.Id):trace(string.format("code = %s", tostring(val))) options.tacanCode = val end if key:lower() == "channel" then -- Set TACAN channel. veaf.loggers.get(veafSpawn.Id):trace(string.format("channel = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.tacanChannel = nVal end if key:lower() == "arrow" then veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword arrow = %s", tostring(val))) options.drawArrow = true end if key:lower() == "fill" then veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword fill = %s", tostring(val))) options.drawFillColor = val end if key:lower() == "color" then veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword color = %s", tostring(val))) options.drawColor = val -- Set smoke color. if (val:lower() == "red") then options.smokeColor = trigger.smokeColor.Red elseif (val:lower() == "green") then options.smokeColor = trigger.smokeColor.Green elseif (val:lower() == "orange") then options.smokeColor = trigger.smokeColor.Orange elseif (val:lower() == "blue") then options.smokeColor = trigger.smokeColor.Blue elseif (val:lower() == "white") then options.smokeColor = trigger.smokeColor.White end end if key:lower() == "skill" then veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword skill = %s", tostring(val))) options.skill = val end if key:lower() == "dist" or key:lower() == "distance" then -- Set distance. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword distance = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.distance = nVal end if options.cargo and key:lower() == "name" then -- Set cargo type. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword name = %s", tostring(val))) options.cargoType = val end if options.cargo and key:lower() == "weight" then -- Set cargo type. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword weight = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) if nVal >= 0 and nVal <= veafSpawn.cargoWeightBiasRange then options.cargoWeightBias = nVal elseif nVal > veafSpawn.cargoWeightBiasRange then options.cargoWeightBias = veafSpawn.cargoWeightBiasRange elseif nVal < 0 then options.cargoWeightBias = 0 end end if key:lower() == "type" then -- Set farp type. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword type = %s", tostring(val))) options.type = val end if options.farp and key:lower() == "nofarpmarkers" then -- Skip the invisible FARP special vehicles that mark the position of the FARP veaf.loggers.get(veafSpawn.Id):trace("Keyword noFarpMarkers is set") options.noFarpMarkers = true end if options.cargo and key:lower() == "smoke" then -- Mark with green smoke. veaf.loggers.get(veafSpawn.Id):trace("Keyword smoke is set") options.cargoSmoke = true end if key:lower() == "size" then -- Set size. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword size = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.size = nVal end if key:lower() == "defense" then -- Set defense. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword defense = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) if nVal >= 0 then options.defense = nVal end end if key:lower() == "armor" then -- Set armor. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword armor = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) if nVal >= 0 then options.armor = nVal end end if key:lower() == "repeat" then -- Set repeat count. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword repeat = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.repeatCount = nVal end if key:lower() == "delay" then -- Set delay. veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword delay = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.repeatDelay = nVal end if key:lower() == "static" then -- Set static unit spawn toggle veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword static found")) options.forceStatic = true end if key:lower() == "immortal" then -- Set spawned unit to invisible and immortal veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword immortal found")) options.immortal = true end if key:lower() == "delayed" then -- Set delayed start on first spawn occurence veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword delayed = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) if nVal >= 0 then options.delayedStart = nVal else options.delayedStart = veafSpawn.MIN_REPEAT_DELAY end end if key:lower() == "showmfd" then -- Set hiddenOnMFD option or not veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword showmfd found")) options.showMFD = true end if key:lower() == "disperse" then -- Set hiddenOnMFD option or not veaf.loggers.get(veafSpawn.Id):trace(string.format("Keyword disperse = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) if nVal >= 0 then options.disperse = nVal end end end -- check mandatory parameter "name" for command "group" if options.group and not(options.name) then return nil end -- check mandatory parameter "name" for command "unit" if options.unit and not(options.name) then return nil end -- check mandatory parameter "name" for all mission master commands if (options.mmFlagOff or options.mmFlagOn or options.mmRun) and not(options.name) then return nil end return options end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Manage drawings on the map ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafSpawn.drawings = {} veafSpawn.drawingsMarkers = {} --- Add a point to a drawing on the map (or start a new drawing) function veafSpawn.addPointToDrawing(point, name, color, fillColor, lineType, isArrow) veaf.loggers.get(veafSpawn.Id):debug(string.format("addPointToDrawing(point=%s, name=%s, color=%s, fillColor=%s, lineType=%s, isArrow=%s)", veaf.p(point), veaf.p(name), veaf.p(color), veaf.p(fillColor), veaf.p(lineType), veaf.p(isArrow))) if not name then veaf.loggers.get(veafSpawn.Id):warn("Name is mandatory for drawing commands") return end local drawing = veafSpawn.drawings[name:lower()] if not drawing then drawing = VeafDrawingOnMap:new():setName(name) veafSpawn.drawings[name:lower()] = drawing end local drawingMarkerId = veafSpawn.drawingsMarkers[name:lower()] if drawingMarkerId then trigger.action.removeMark(drawingMarkerId) end drawingMarkerId = veaf.getUniqueIdentifier() trigger.action.markToAll(drawingMarkerId, name, point, true) veafSpawn.drawingsMarkers[name:lower()] = drawingMarkerId if color then drawing:setColor(color) end if lineType then drawing:setLineType(lineType) end if isArrow then drawing:setArrow() end if fillColor then drawing:setFillColor(fillColor) end drawing:addPoint(point) drawing:draw() end --- Add a circle to the map function veafSpawn.drawCircle(point, name, radius, color, fillColor, lineType) veaf.loggers.get(veafSpawn.Id):debug(string.format("drawCircle(point=%s, name=%s, radius=%s, color=%s, fillColor=%s, lineType=%s)", veaf.p(point), veaf.p(name), veaf.p(radius), veaf.p(color), veaf.p(fillColor), veaf.p(lineType))) if not name then veaf.loggers.get(veafSpawn.Id):warn("Name is mandatory for drawing commands") return end local drawing = veafSpawn.drawings[name:lower()] if drawing then -- erase the old one first drawing:erase() veafSpawn.drawings[name:lower()] = nil end drawing = VeafCircleOnMap:new():setName(name) drawing:setCenter(point) drawing:setRadius(radius or 5000) veafSpawn.drawings[name:lower()] = drawing local drawingMarkerId = veafSpawn.drawingsMarkers[name:lower()] if drawingMarkerId then trigger.action.removeMark(drawingMarkerId) end drawingMarkerId = veaf.getUniqueIdentifier() trigger.action.markToAll(drawingMarkerId, name, point, true) veafSpawn.drawingsMarkers[name:lower()] = drawingMarkerId if color then drawing:setColor(color) end if lineType then drawing:setLineType(lineType) end if fillColor then drawing:setFillColor(fillColor) end drawing:draw() end --- Add a square to the map function veafSpawn.drawSquare(point, name, side, color, fillColor, lineType) veaf.loggers.get(veafSpawn.Id):debug(string.format("drawSquare(point=%s, name=%s, side=%s, color=%s, fillColor=%s, lineType=%s)", veaf.p(point), veaf.p(name), veaf.p(side), veaf.p(color), veaf.p(fillColor), veaf.p(lineType))) if not name then veaf.loggers.get(veafSpawn.Id):warn("Name is mandatory for drawing commands") return end local drawing = veafSpawn.drawings[name:lower()] if drawing then -- erase the old one first drawing:erase() veafSpawn.drawings[name:lower()] = nil end drawing = VeafSquareOnMap:new():setName(name) drawing:setCenter(point) drawing:setSide(side or 5000) veafSpawn.drawings[name:lower()] = drawing local drawingMarkerId = veafSpawn.drawingsMarkers[name:lower()] if drawingMarkerId then trigger.action.removeMark(drawingMarkerId) end drawingMarkerId = veaf.getUniqueIdentifier() trigger.action.markToAll(drawingMarkerId, name, point, true) veafSpawn.drawingsMarkers[name:lower()] = drawingMarkerId if color then drawing:setColor(color) end if lineType then drawing:setLineType(lineType) end if fillColor then drawing:setFillColor(fillColor) end drawing:draw() end --- Erase drawing from the map function veafSpawn.eraseDrawing(name) veaf.loggers.get(veafSpawn.Id):debug(string.format("eraseDrawing(name=%s)",veaf.p(name))) if not name then veaf.loggers.get(veafSpawn.Id):warn("Name is mandatory for drawing commands") return end local drawing = veafSpawn.drawings[name:lower()] if not drawing then local message = string.format("Could not find a drawing named %s", veaf.p(name)) veaf.loggers.getSpawn(veaf.Id):warn(message) trigger.action.outText(message, 5) return end drawing:erase() veafSpawn.drawings[name:lower()] = nil local drawingMarkerId = veafSpawn.drawingsMarkers[name:lower()] if drawingMarkerId then trigger.action.removeMark(drawingMarkerId) end veafSpawn.drawingsMarkers[name:lower()] = nil end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Group spawn command ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Spawn a specific group at a specific spot function veafSpawn.doSpawnGroup(spawnSpot, radius, groupDefinition, czName, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD, shuffle) veaf.loggers.get(veafSpawn.Id):debug("doSpawnGroup(czName=%s, country=%s, alt=%s, hdg=%s, spacing=%s, groupName=%s, silent=%s, hasDest=%s, hiddenOnMFD=%s, shuffle=%s)", czName, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD, shuffle) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot)) veafSpawn.spawnedUnitsCounter = veafSpawn.spawnedUnitsCounter + 1 if type(groupDefinition) == "string" then local name = groupDefinition -- find the desired group in the groups database groupDefinition = veafUnits.findGroup(name) if not(groupDefinition) then veaf.loggers.get(veafSpawn.Id):info("cannot find group "..name) if not(silent) then trigger.action.outText("cannot find group "..name, 5) end return nil end end veaf.loggers.get(veafSpawn.Id):trace("doSpawnGroup: groupDefinition.description=" .. groupDefinition.description) local units = {} -- place group units on the map local group, cells = veafUnits.placeGroup(groupDefinition, spawnSpot, spacing, hdg, hasDest) veafUnits.traceGroup(group, cells) if not(groupName) then groupName = group.groupName or "spawned group" end -- use the centralized group naming function groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), groupName, czName) if hasDest then mist.scheduleFunction(veafUnits.removePathfindingFixUnit,{groupName}, timer.getTime()+veafUnits.delayBeforePathfindingFix) end for i=1, #group.units do local unit = group.units[i] local unitType = unit.typeName local unitName = groupName .. " / " .. unit.displayName .. " #" .. i local spawnPoint = unit.spawnPoint if alt > 0 then spawnPoint.y = alt end -- check if position is correct for the unit type if not veafUnits.checkPositionForUnit(spawnPoint, unit) then veaf.loggers.get(veafSpawn.Id):info("cannot find a suitable position for spawning unit ".. unitType) if not(silent) then trigger.action.outText("cannot find a suitable position for spawning unit "..unitType, 5) end else local toInsert = { ["x"] = spawnPoint.x, ["y"] = spawnPoint.z, ["alt"] = spawnPoint.y, ["type"] = unitType, ["name"] = unitName, ["speed"] = 0, -- speed in m/s ["skill"] = "Random", ["heading"] = spawnPoint.hdg } veaf.loggers.get(veafSpawn.Id):trace(string.format("toInsert x=%.1f y=%.1f, alt=%.1f, type=%s, name=%s, speed=%d, heading=%d, skill=%s, country=%s", veaf.p(toInsert.x), veaf.p(toInsert.y), veaf.p(toInsert.alt), veaf.p(toInsert.type), veaf.p(toInsert.name), veaf.p(toInsert.speed), veaf.p(mist.utils.toDegree(toInsert.heading)), veaf.p(toInsert.skill), veaf.p(country))) table.insert(units, toInsert) end end -- shuffle the group if needed (useful for randomizing convoys) -- counter productive with hasDest which to speed up convoys orders all of the units so that they spawn in order and in a line -- the best way to execute this shuffle is to create groups with random cells for each unit, TBD if shuffle and not hasDest then units = veaf.shuffle(units) end -- actually spawn the group if group.naval then mist.dynAdd({country = country, category = "SHIP", name = groupName, hidden = false, units = units, hiddenOnMFD = hiddenOnMFD}) elseif group.air then mist.dynAdd({country = country, category = "AIRPLANE", name = groupName, hidden = false, units = units, hiddenOnMFD = hiddenOnMFD}) else mist.dynAdd({country = country, category = "GROUND_UNIT", name = groupName, hidden = false, units = units, hiddenOnMFD = hiddenOnMFD}) end if not(silent) then -- message the group spawning trigger.action.outText("A " .. group.description .. "("..country..") has been spawned", 5) end return groupName end --- Spawn a FARP function veafSpawn.spawnFarp(spawnSpot, radius, name, country, farptype, side, hdg, spacing, silent, hiddenOnMFD, noFarpMarkers, code, freq, mod) veaf.loggers.get(veafSpawn.Id):debug("spawnFarp(name=%s, country=%s, farptype=%s, side=%s, hdg=%s, spacing=%s, silent=%s, hiddenOnMFD=%s, noFarpMarkers=%s)",veaf.p(name), veaf.p(country), veaf.p(farptype), veaf.p(side), veaf.p(hdg), veaf.p(spacing), veaf.p(silent), veaf.p(hiddenOnMFD), veaf.p(noFarpMarkers)) local radius = radius or 0 local name = name local hdg = hdg or 0 local side = side or 1 local country = country or "usa" local farptype = farptype or "" local noFarpMarkers = noFarpMarkers or false local spawnPosition = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace("spawnPosition=%s", veaf.p(spawnPosition)) if not name or name == "" then local _lat, _lon = coord.LOtoLL(spawnSpot) veaf.loggers.get(veafSpawn.Id):trace("_lat=%s", veaf.p(_lat)) veaf.loggers.get(veafSpawn.Id):trace("_lon=%s", veaf.p(_lon)) local _mgrs = coord.LLtoMGRS(_lat, _lon) veaf.loggers.get(veafSpawn.Id):trace("_mgrs=%s", veaf.p(_mgrs)) local _UTM = _mgrs.MGRSDigraph .. math.floor(_mgrs.Easting / 1000) .. math.floor(_mgrs.Northing / 1000) name = "FARP ".. _UTM:upper() .. "-" .. timer.getTime() end local _type = "Invisible FARP" local _shape = "invisiblefarp" if farptype:lower() == "quad" then _type = "FARP" _shape = "FARPs" elseif farptype:lower() == "single" then _type = "FARP" _shape = "FARP" elseif farptype:lower() == "pad" then _type = "FARP_SINGLE_01" _shape = "FARP_SINGLE_01" elseif farptype:lower() == "invisible" then _type = "Invisible FARP" _shape = "invisiblefarp" end -- spawn the FARP local _farpStatic = { ["category"] = "Heliports", ["shape_name"] = _shape, ["type"] = _type, ["unitId"] = mist.getNextUnitId(), ["y"] = spawnPosition.z, ["x"] = spawnPosition.x, ["groupName"] = name, ["name"] = name, ["canCargo"] = false, ["heading"] = mist.utils.toRadian(hdg), ["country"] = country, ["coalition"] = side, ["dead"] = false, ["dynamicCargo"] = true, ["dynamicSpawn"] = true, ["allowHotStart"] = true, --["unlimitedAircrafts"] = true, ["unlimitedFuel"] = true, ["unlimitedMunitions"] = true, } mist.dynAddStatic(_farpStatic) local _spawnedFARP = StaticObject.getByName(name) veaf.loggers.get(veafSpawn.Id):trace("_spawnedFARP=%s", veaf.p(_spawnedFARP)) if _spawnedFARP then veaf.loggers.get(veafSpawn.Id):debug("Spawned the FARP static %s", veaf.p(name)) -- populate the FARP but make the units invisible to MFDs as they are redundant (FARP already shows if wanted) veafGrass.buildFarpUnits(_farpStatic, nil, name, hiddenOnMFD, noFarpMarkers, code, freq, mod) end return name end --- Spawn a FARP function veafSpawn.spawnFob(spawnSpot, radius, name, country, fobtype, side, hdg, spacing, silent, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnFob(name=%s, country=%s, fobtype=%s, side=%s, hdg=%s, spacing=%s, silent=%s, hiddenOnMFD=%s)",veaf.p(name), veaf.p(country), veaf.p(fobtype), veaf.p(side), veaf.p(hdg), veaf.p(spacing), veaf.p(silent), veaf.p(hiddenOnMFD)) local TOWER_DISTANCE = 20 local BEACON_DISTANCE = 3 if not veaf.ctld_initialized then veaf.loggers.get(veafSpawn.Id):error("spawnFob([%s]): cannot spawn FOB without CTLD!)",veaf.p(name)) return nil end local _radius = radius or 0 local _fobName = name local _side = side or 1 local _country = country or "usa" local _fobtype = fobtype or "" -- only a single FOB type in CTLD, yet local _hdg = hdg or 0 local _spawnPosition = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, _radius)) veaf.loggers.get(veafSpawn.Id):trace("spawnPosition=%s", veaf.p(_spawnPosition)) if not _fobName or _fobName == "" then local _lat, _lon = coord.LOtoLL(spawnSpot) veaf.loggers.get(veafSpawn.Id):trace("_lat=%s", veaf.p(_lat)) veaf.loggers.get(veafSpawn.Id):trace("_lon=%s", veaf.p(_lon)) local _mgrs = coord.LLtoMGRS(_lat, _lon) veaf.loggers.get(veafSpawn.Id):trace("_mgrs=%s", veaf.p(_mgrs)) local _UTM = _mgrs.MGRSDigraph .. math.floor(_mgrs.Easting / 1000) .. math.floor(_mgrs.Northing / 1000) _fobName = "FOB ".. _UTM:upper() end -- make name unique _fobName = string.format("%s #%i", _fobName, veaf.getUniqueIdentifier()) -- spawn the FOB buildings local _outpost = { category = "Fortifications", type = "outpost", y = _spawnPosition.z, x = _spawnPosition.x, name = _fobName, canCargo = false, heading = mist.utils.toRadian(hdg), country = _country } mist.dynAddStatic(_outpost) local _fob = StaticObject.getByName(_outpost["name"]) local _tower = { type = "house2arm", rate = 100, y = _outpost.y + TOWER_DISTANCE * math.sin(mist.utils.toRadian(_hdg)), x = _outpost.x + TOWER_DISTANCE * math.cos(mist.utils.toRadian(_hdg)), name = _fobName .. " Watchtower #002", category = "Fortifications", canCargo = false, heading = mist.utils.toRadian(hdg), country = _country } mist.dynAddStatic(_tower) --make it able to deploy crates and pickup troops table.insert(ctld.logisticUnits, _fobName) table.insert(ctld.builtFOBS, _fobName) -- add the FOB to the named points local _namedPoint = _spawnPosition _namedPoint.atc = true _namedPoint.runways = {} -- spawn a beacon local _beaconPoint = { z = _tower.y + BEACON_DISTANCE * math.sin(mist.utils.toRadian(_hdg)), x = _tower.x + BEACON_DISTANCE * math.cos(mist.utils.toRadian(_hdg)), y = _spawnPosition.y, } ctld.beaconCount = ctld.beaconCount + 1 local _radioBeaconName = "FOB Beacon #" .. ctld.beaconCount local _radioBeaconDetails = ctld.createRadioBeacon(_beaconPoint, _side, _country, _radioBeaconName, nil, true) ctld.fobBeacons[_fobName] = { vhf = _radioBeaconDetails.vhf, uhf = _radioBeaconDetails.uhf, fm = _radioBeaconDetails.fm } if _radioBeaconDetails ~= nil then _namedPoint.tacan = string.format("ADF : %.2f KHz - %.2f MHz - %.2f MHz FM", _radioBeaconDetails.vhf / 1000, _radioBeaconDetails.uhf / 1000000, _radioBeaconDetails.fm / 1000000) veaf.loggers.get(veafSpawn.Id):trace("_namedPoint.tacan=%s", veaf.p(_namedPoint.tacan)) end trigger.action.outTextForCoalition(_side, string.format("Finished building FOB %s! Crates and Troops can now be picked up.", _fobName), 10) _namedPoint.tower = "No Control" veaf.loggers.get(veafSpawn.Id):trace("_namedPoint=%s", veaf.p(_namedPoint)) veafNamedPoints.addPoint(_fobName, _namedPoint) veaf.loggers.get(veafSpawn.Id):info("Spawned FOB %s", veaf.p(_fobName)) return _fobName end --- Spawn a specific group at a specific spot function veafSpawn.spawnGroup(spawnSpot, radius, name, czName, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnGroup(name=%s, czName=%s, country=%s, alt=%s, hdg=%s, spacing=%s, groupName=%s, silent=%s, hiddenOnMFD=%s)", name, czName, country, alt, hdg, spacing, silent, groupName, hiddenOnMFD) local spawnedGroupName = veafSpawn.doSpawnGroup(spawnSpot, radius, name, czName, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD) return spawnedGroupName end local function validateSpawnPosition(spawnPosition, unit, silent) if not veafUnits.checkPositionForUnit(spawnPosition, unit) then veaf.loggers.get(veafSpawn.Id):info("Cannot find a suitable position for spawning unit " .. unit.typeName) if not silent then trigger.action.outText("Cannot find a suitable position for spawning unit " .. unit.typeName, 5) end return false end return true end function veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD, hasDest) veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn._createDcsUnits([%s])",country or "")) if hasDest then mist.scheduleFunction(veafUnits.removePathfindingFixUnit,{groupName}, timer.getTime()+veafUnits.delayBeforePathfindingFix) end local dcsUnits = {} for i=1, #units do local unit = units[i] local unitType = unit.typeName local unitNameTemplate = "%s - %s" if veafSpawn.HideTypeFromGroupNames then unitNameTemplate = "%s" end local unitName = string.format(unitNameTemplate, groupName, unit.displayName) local spawnPosition = unit.spawnPoint local hdg = spawnPosition.hdg or math.random(0, 359) if validateSpawnPosition(spawnPosition, unit, false) then local toInsert = { ["x"] = spawnPosition.x, ["y"] = spawnPosition.z, ["alt"] = spawnPosition.y, ["type"] = unitType, ["name"] = unitName, ["speed"] = 0, ["skill"] = "Excellent", ["heading"] = hdg } veaf.loggers.get(veafSpawn.Id):trace(string.format("toInsert x=%.1f y=%.1f, alt=%.1f, type=%s, name=%s, speed=%d, heading=%d, skill=%s, country=%s", toInsert.x, toInsert.y, toInsert.alt, toInsert.type, toInsert.name, toInsert.speed, toInsert.heading, toInsert.skill, country )) table.insert(dcsUnits, toInsert) end end -- actually spawn groups mist.dynAdd({country = country, category = "GROUND_UNIT", name = groupName, hidden = false, units = dcsUnits, hiddenOnMFD = hiddenOnMFD}) end --- Spawns a dynamic infantry group function veafSpawn.spawnInfantryGroup(spawnSpot, radius, czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnInfantryGroup(czName=%s, country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot)) local groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Infantry Section", czName) local group = veafCasMission.generateInfantryGroup(groupName, defense, armor, side, size) local group = veafUnits.processGroup(group) local groupPosition = veaf.placePointOnLand(spawnSpot) veaf.loggers.get(veafSpawn.Id):trace(string.format("groupPosition = %s",veaf.vecToString(groupPosition))) local group, cells = veafUnits.placeGroup(group, groupPosition, spacing, heading) -- shuffle the units in the group local units = veaf.shuffle(group.units) veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD) if not silent then trigger.action.outText("Spawned dynamic infantry group "..groupName, 5) end return groupName end --- Spawns a dynamic armored platoon function veafSpawn.spawnArmoredPlatoon(spawnSpot, radius, czName, country, side, heading, spacing, defense, armor, size, silent, hasDest, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnArmoredPlatoon(czName=%s, country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s",spawnSpot) local randSpot = mist.getRandPointInCircle(spawnSpot, radius) veaf.loggers.get(veafSpawn.Id):trace("randSpot=%s",randSpot) local spawnSpot = veaf.placePointOnLand(randSpot) veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s",spawnSpot) local groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Armored Platoon", czName) veaf.loggers.get(veafSpawn.Id):trace("groupName=%s",groupName) local group = veafCasMission.generateArmorPlatoon(groupName, defense, armor, side, size) local group = veafUnits.processGroup(group) local groupPosition = veaf.placePointOnLand(spawnSpot) veaf.loggers.get(veafSpawn.Id):trace("groupPosition=%s",groupPosition) local group, cells = veafUnits.placeGroup(group, groupPosition, spacing, heading, hasDest) -- shuffle the units in the group local units = group.units if not(hasDest) then units = veaf.shuffle(group.units) end veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD, hasDest) if not silent then trigger.action.outText("Spawned dynamic armored platoon "..groupName, 5) end return groupName end --- Spawns a dynamic air defense battery function veafSpawn.spawnAirDefenseBattery(spawnSpot, radius, czName, country, side, heading, spacing, defense, silent, hasDest, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnAirDefenseBattery(czName=%s, country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot)) local groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Air Defense Battery", czName) local group = veafCasMission.generateAirDefenseGroup(groupName, defense, side) local group = veafUnits.processGroup(group) local groupPosition = veaf.placePointOnLand(spawnSpot) veaf.loggers.get(veafSpawn.Id):trace(string.format("groupPosition = %s",veaf.vecToString(groupPosition))) local group, cells = veafUnits.placeGroup(group, groupPosition, spacing, heading, hasDest) -- shuffle the units in the group local units = group.units if not(hasDest) then units = veaf.shuffle(group.units) end veafSpawn._createDcsUnits(country or veaf.getCountryForCoalition(side), units, groupName, hiddenOnMFD, hasDest) if not silent then trigger.action.outText("Spawned dynamic air defense battery "..groupName, 5) end return groupName end --- Spawns a dynamic transport company function veafSpawn.spawnTransportCompany(spawnSpot, radius, czName, country, side, heading, spacing, defense, size, silent, hasDest, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnTransportCompany(czName=%s, country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot)) local groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Transport Company", czName) local group = veafCasMission.generateTransportCompany(groupName, defense, side, size) local group = veafUnits.processGroup(group) local groupPosition = veaf.placePointOnLand(spawnSpot) veaf.loggers.get(veafSpawn.Id):trace(string.format("groupPosition = %s",veaf.vecToString(groupPosition))) local group, cells = veafUnits.placeGroup(group, groupPosition, spacing, heading, hasDest) -- shuffle the units in the group local units = group.units if not(hasDest) then units = veaf.shuffle(group.units) end veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD, hasDest) if not silent then trigger.action.outText("Spawned dynamic transport company "..groupName, 5) end return groupName end --- Spawns a dynamic full combat group composed of multiple platoons function veafSpawn.spawnFullCombatGroup(spawnSpot, radius, czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnFullCombatGroup(czName=%s, country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", czName, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot)) local groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Full Combat Group", czName) local groupPosition = veaf.placePointOnLand(spawnSpot) local units = veafCasMission.generateCasGroup(groupName, groupPosition, size, defense, armor, spacing, side) veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD) if not silent then trigger.action.outText("Spawned full combat group "..groupName, 5) end return groupName end --- Spawn a specific group at a specific spot function veafSpawn.spawnConvoy(spawnSpot, name, czName, radius, country, side, heading, spacing, speed, patrol, offroad, destination, defense, size, armor, silent, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnConvoy(spawnSpot=[%s], name=[%s], radius=[%s], country=[%s], side=[%s], speed=[%s], patrol=[%s], offroad=[%s], destination=[%s], defense=[%s], size=[%s], armor=[%s], silent=[%s], hiddenOnMFD=[%s])", czName, spawnSpot, name, radius, country, side, speed, patrol, offroad, destination, defense, size, armor, silent, hiddenOnMFD) if not(destination) then trigger.action.outText("No destination enterred !", 5) return false end local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot)) -- check that destination exists local point = nil if destination then point = veafNamedPoints.getPoint(destination) end if not(point) then local _lat, _lon = veaf.computeLLFromString(destination) veaf.loggers.get(veafSpawn.Id):trace(string.format("_lat=%s",veaf.p(_lat))) veaf.loggers.get(veafSpawn.Id):trace(string.format("_lon=%s",veaf.p(_lon))) if _lat and _lon then point = coord.LLtoLO(_lat, _lon) veaf.loggers.get(veafSpawn.Id):trace(string.format("point=%s",veaf.p(point))) end end if not(point) then trigger.action.outText("A point named "..destination.." cannot be found, and these are not valid coordinates !", 5) return false end local groupUnits = {} groupUnits.units = {} local groupId = math.random(99999) local groupName = name if not groupName or groupName == "" then groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), "Convoy", czName) end -- generate the transport vehicles and air defense if size and size > 0 then -- this is only for reading clarity sake -- generate the group local group = veafCasMission.generateTransportCompany(groupId, defense, side, size) -- process the group local group = veafUnits.processGroup(group) -- add the units to the global units list for _,u in pairs(group.units) do table.insert(groupUnits.units, u) end end -- generate the armored vehicles if armor and armor > 0 then -- generate the group local group = veafCasMission.generateArmorPlatoon(groupId, defense, armor, side, size / 2) -- process the group local group = veafUnits.processGroup(group) -- add the units to the global units list for _,u in pairs(group.units) do table.insert(groupUnits.units, u) end end if groupUnits.units then -- place its units local groupUnits, cells = veafUnits.placeGroup(groupUnits, veaf.placePointOnLand(spawnSpot), spacing, heading, true) veafUnits.traceGroup(groupUnits, cells) -- shuffle the units in the convoy --disabled the shuffle to not have interractions with the line spawn put in place for faster departure times, which shuffles units anyways --units = veaf.shuffle(units) veafSpawn._createDcsUnits(country, groupUnits.units, groupName, hiddenOnMFD, true) local route = veaf.generateVehiclesRoute(spawnSpot, destination, not offroad, speed, patrol, groupName) veafSpawn.spawnedConvoys[groupName] = {route=route, name=groupName} -- make the group go to destination veaf.loggers.get(veafSpawn.Id):trace("make the group go to destination : ".. groupName) mist.goRoute(groupName, route) if not silent then trigger.action.outText("Spawned convoy "..groupName, 5) end end return groupName end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Unit spawn command ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Spawn a specific unit at a specific spot -- @param position spawnPosition -- @param string name -- @param string country -- @param int speed -- @param int alt -- @param int speed -- @param int hdg (0..359) -- @param string unitName (callsign) -- @param string role (ex: jtac) -- @param boolean static (is the unit force to spawn as a static unit) -- @param integer code (starts at 1111, laser code if jtac) -- @param string freq (frequency if JTAC in MHz with . separator) -- @param boolean silent (mutes messages to players except errors) -- @param boolean hiddenOnMFD function veafSpawn.spawnUnit(spawnPosition, radius, name, czName, country, alt, hdg, unitName, role, static, code, freq, mod, silent, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnUnit(name = %s, czName=%s, country=%s, alt=%d, hdg=%d, unitName=%s, role=%s, static=%s, code=%s, freq=%s, mod=%s, silent=%s, hiddenOnMFD=%s)", name, czName, country, alt, hdg, unitName, role, static, code, freq, mod, silent, hiddenOnMFD) veafSpawn.spawnedUnitsCounter = veafSpawn.spawnedUnitsCounter + 1 -- find the desired unit in the groups database local unit = veafUnits.findUnit(name) if not(unit) then veaf.loggers.get(veafSpawn.Id):info("cannot find unit "..name) trigger.action.outText("cannot find unit "..name, 5) return end -- cannot spawn planes or helos yet [TODO], however spawning them as a static is fine if unit.air and not static then veaf.loggers.get(veafSpawn.Id):info("Air units cannot be spawned at the moment (work in progress)") trigger.action.outText("Air units cannot be spawned at the moment (work in progress)", 5) return end local units = {} local groupName = nil veaf.loggers.get(veafSpawn.Id):trace("spawnUnit unit = " .. unit.displayName .. ", dcsUnit = " .. tostring(unit.typeName)) if role == "jtac" then local name = "JTAC " .. tostring(code):sub(1,1) .. " " .. tostring(code):sub(2,2) .. " " .. tostring(code):sub(3,3) .. " " .. tostring(code):sub(4,4) veaf.loggers.get(veafSpawn.Id):trace(string.format("name=%s", tostring(name))) groupName = name unitName = name elseif role == "tacan" then local name = "TACAN " .. tostring(freq)..tostring(mod) veaf.loggers.get(veafSpawn.Id):trace(string.format("name=%s", tostring(name))) groupName = name unitName = name else groupName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), name, czName) if not unitName then unitName = veaf.getNameForSpawnedGroup(veaf.getCoalitionForCountry(country, true), unit.displayName, czName) end end veaf.loggers.get(veafSpawn.Id):trace("groupName="..groupName) veaf.loggers.get(veafSpawn.Id):trace("unitName="..unitName) local spawnSpot = nil local nbTries = 25 repeat spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnPosition, radius)) veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnUnit: spawnSpot x=%.1f y=%.1f, z=%.1f", spawnSpot.x, spawnSpot.y, spawnSpot.z)) if alt > 0 then spawnSpot.y = alt end if not veafUnits.checkPositionForUnit(spawnSpot, unit) then veaf.loggers.get(veafSpawn.Id):debug("finding another spawnSpot for unit %s, remaining tries #%s", unit.displayName, nbTries) spawnSpot = nil nbTries = nbTries - 1 end until spawnSpot or nbTries <= 0 if not spawnSpot then veaf.loggers.get(veafSpawn.Id):info("cannot find a suitable position for spawning unit "..unit.displayName) trigger.action.outText("cannot find a suitable position for spawning unit "..unit.displayName, 5) return else local toInsert = {} local effectPreset = nil local effectTransparency = nil local shapeName = nil if unit.static or static then if unit.category then if unit.category == "Heliport" then unit.category = "Heliports" end -- if unit.category == "Effect" then -- unit.category = "Effects" -- effectPreset = 2 -- effectTransparency = 1 -- shapeName = "medium smoke and fire" -- end end groupName = unitName --this name here will be used for reference by DCS, since we return groupName for other scripts to do their thing, this must be the unitName toInsert = { ["x"] = spawnSpot.x, ["y"] = spawnSpot.z, ["alt"] = spawnSpot.y, ["type"] = unit.typeName, ["name"] = groupName, ["category"] = unit.category, ["heading"] = mist.utils.toRadian(hdg), -- ["effectTransparency"] = effectTransparency, -- ["effectPreset"] = effectPreset, -- ["shapeName"] = shapeName, } else toInsert = { ["x"] = spawnSpot.x, ["y"] = spawnSpot.z, ["alt"] = spawnSpot.y, ["type"] = unit.typeName, ["name"] = unitName, ["speed"] = 0, ["skill"] = "Random", ["heading"] = mist.utils.toRadian(hdg), } end table.insert(units, toInsert) end veaf.loggers.get(veafSpawn.Id):trace(string.format("unitData = %s", veaf.p(units))) -- actually spawn the unit if unit.static or static then --if the unit was forced to spawn as a static it could still be an air or a naval unit so this check goes first veaf.loggers.get(veafSpawn.Id):trace("Spawning STATIC") mist.dynAddStatic({country = country, groupName = groupName, units = units, hiddenOnMFD = hiddenOnMFD}) --groupName = nil --statics do not have a group name, you must set groupName to nil to avoid other scripts interacting elseif unit.air then veaf.loggers.get(veafSpawn.Id):trace("Spawning AIRPLANE") mist.dynAdd({country = country, category = "PLANE", groupName = groupName, units = units, hiddenOnMFD = hiddenOnMFD}) elseif unit.naval then veaf.loggers.get(veafSpawn.Id):trace("Spawning SHIP") mist.dynAdd({country = country, category = "SHIP", groupName = groupName, units = units, hiddenOnMFD = hiddenOnMFD}) else veaf.loggers.get(veafSpawn.Id):trace("Spawning GROUND_UNIT") mist.dynAdd({country = country, category = "GROUND_UNIT", groupName = groupName, units = units, hiddenOnMFD = hiddenOnMFD}) end if role == "jtac" and not static then -- JTAC needs to be invisible and immortal local _setImmortal = { id = 'SetImmortal', params = { value = true } } -- invisible to AI, Shagrat local _setInvisible = { id = 'SetInvisible', params = { value = true } } local spawnedGroup = Group.getByName(groupName) local controller = spawnedGroup:getController() Controller.setCommand(controller, _setImmortal) Controller.setCommand(controller, _setInvisible) -- start lasing if ctld then ctld.cleanupJTAC(groupName) local radioData = {freq=freq, mod=mod, name=groupName} veafSpawn.JTACAutoLase(groupName, code, radioData) end elseif role == "tacan" and not static then veaf.loggers.get(veafSpawn.Id):trace(string.format("name=%s", tostring(name))) veaf.loggers.get(veafSpawn.Id):trace(string.format("freq=%s", tostring(freq))) local mod = string.upper(mod) or "X" veaf.loggers.get(veafSpawn.Id):trace(string.format("mod=%s", tostring(mod))) local txFreq = (1025 + freq - 1) * 1000000 local rxFreq = (962 + freq - 1) * 1000000 if (freq < 64 and mod == "Y") or (freq >= 64 and mod == "X") then rxFreq = (1088 + freq - 1) * 1000000 end veaf.loggers.get(veafSpawn.Id):trace(string.format("txFreq=%s", tostring(txFreq))) veaf.loggers.get(veafSpawn.Id):trace(string.format("rxFreq=%s", tostring(rxFreq))) local command = { id = 'ActivateBeacon', params = { type = 4, system = 18, callsign = code or "TCN", frequency = rxFreq, AA = false, channel = freq, bearing = true, modeChannel = mod, } } veaf.loggers.get(veafSpawn.Id):trace(string.format("setting %s", veaf.p(command))) local spawnedGroup = Group.getByName(groupName) local controller = spawnedGroup:getController() controller:setCommand(command) veaf.loggers.get(veafSpawn.Id):trace(string.format("done setting command")) end -- message the unit spawning veaf.loggers.get(veafSpawn.Id):trace(string.format("message the unit spawning")) if (role == "jtac") or not silent then local message = "A " .. unit.displayName .. " ("..country..") has been spawned" if role == "jtac" and not static then message = "JTAC spawned, lasing on "..code..", available on "..freq.." "..mod end veaf.loggers.get(veafSpawn.Id):trace(message) trigger.action.outText(message, 15) end return groupName end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Cargo spawn command ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Spawn a specific cargo at a specific spot function veafSpawn.spawnCargo(spawnSpot, radius, cargoType, country, weightBias, cargoSmoke, unitName, silent, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnCargo(cargoType = " .. cargoType ..")") local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnCargo: spawnSpot x=%.1f y=%.1f, z=%.1f", spawnSpot.x, spawnSpot.y, spawnSpot.z)) return veafSpawn.doSpawnCargo(spawnSpot, radius, cargoType, country, weightBias, unitName, cargoSmoke, silent, hiddenOnMFD) end --- Spawn a logistic unit for CTLD at a specific spot function veafSpawn.spawnLogistic(spawnSpot, radius, country, silent, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("spawnLogistic()") local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnLogistic: spawnSpot x=%.1f y=%.1f, z=%.1f", spawnSpot.x, spawnSpot.y, spawnSpot.z)) local unitName = veafSpawn.doSpawnStatic(spawnSpot, radius, veafSpawn.LogisticUnitCategory, veafSpawn.LogisticUnitType, country, nil, false, true, hiddenOnMFD) if unitName then veaf.loggers.get(veafSpawn.Id):debug(string.format("spawnLogistic: inserting %s into CTLD logistics list", unitName)) if ctld then table.insert(ctld.logisticUnits, unitName) end -- message the unit spawning if not silent then local message = "Logistic unit " .. unitName .. " has been spawned and was added to CTLD." trigger.action.outText(message, 15) end return unitName else local message = "Logistic unit could not be spawned" trigger.action.outText(message, 15) return end end --- Spawn a specific cargo at a specific spot function veafSpawn.doSpawnCargo(spawnSpot, radius, cargoType, country, weightBias, unitName, cargoSmoke, silent, hiddenOnMFD) local weightBias = weightBias or 2 local radius = radius or 0 veaf.loggers.get(veafSpawn.Id):debug("spawnCargo(cargoType = " .. cargoType ..")") local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnCargo: spawnSpot x=%.1f y=%.1f, z=%.1f", spawnSpot.x, spawnSpot.y, spawnSpot.z)) local units = {} local spawnPosition = veaf.findPointInZone(spawnSpot) -- check spawned position validity if spawnPosition == nil then veaf.loggers.get(veafSpawn.Id):info("cannot find a suitable position for spawning cargo "..cargoType) trigger.action.outText("cannot find a suitable position for spawning cargo "..cargoType, 5) return end veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnCargo: spawnPosition x=%.1f y=%.1f", spawnPosition.x, spawnPosition.y)) -- compute cargo weight local cargoWeight = 250 local unit = veafUnits.findDcsUnit(cargoType) if not unit then cargoType = cargoType.. "_cargo" unit = veafUnits.findDcsUnit(cargoType) end if unit then if unit.type then cargoType = unit.type else veaf.loggers.get(veafSpawn.Id):info("could not find cargo type named ".. veaf.p(cargoType)) trigger.action.outText("could not find cargo type named ".. veaf.p(cargoType), 15) return end veaf.loggers.get(veafSpawn.Id):debug(string.format("weightBias=%s", veaf.p(weightBias))) if unit.desc and unit.desc.minMass and unit.desc.maxMass then local weightScaleRange = veafSpawn.cargoWeightBiasRange + 1 local massDelta = unit.desc.maxMass - unit.desc.minMass if massDelta < 0 then --never can be too careful around DCS local temp = unit.desc.maxMass unit.desc.maxMass = unit.desc.minMass unit.desc.minMass = temp massDelta = math.abs(massDelta) end local minMass = unit.desc.minMass + weightBias * massDelta / weightScaleRange local maxMass = unit.desc.minMass + (weightBias+1) * massDelta / weightScaleRange veaf.loggers.get(veafSpawn.Id):debug(string.format("cargo minMass=%s, cargo maxMass=%s", veaf.p(minMass), veaf.p(maxMass))) cargoWeight = math.random(minMass,maxMass) elseif unit.defaultMass then local BiasOffset = -math.floor(veafSpawn.cargoWeightBiasRange / 2) local weightBiasCentered = weightBias + BiasOffset local cargoWeightBiasScaleMin = BiasOffset local cargoWeightBiasScaleMax = veafSpawn.cargoWeightBiasRange + BiasOffset local weightBiasMax = weightBiasCentered + 1 local weightBiasMin = weightBiasCentered cargoWeight = unit.defaultMass veaf.loggers.get(veafSpawn.Id):debug(string.format("cargo defaultMass=%s", veaf.p(cargoWeight))) local minMass = cargoWeight + weightBiasMin * cargoWeight / (2*cargoWeightBiasScaleMax) local maxMass = cargoWeight + weightBiasMax * cargoWeight / (2*cargoWeightBiasScaleMax) veaf.loggers.get(veafSpawn.Id):debug(string.format("cargo minMass=%s, cargo maxMass=%s", veaf.p(minMass), veaf.p(maxMass))) cargoWeight = math.random(minMass,maxMass) end if cargoWeight then veaf.loggers.get(veafSpawn.Id):debug(string.format("cargo mass=%s", veaf.p(cargoWeight))) if not(unitName) then veafSpawn.spawnedUnitsCounter = veafSpawn.spawnedUnitsCounter + 1 unitName = unit.name .. " #" .. veafSpawn.spawnedUnitsCounter end -- create the cargo local cargoTable = { type = cargoType, country = country, category = 'Cargos', name = unitName, x = spawnPosition.x, y = spawnPosition.y, canCargo = true, mass = cargoWeight, hiddenOnMFD = hiddenOnMFD, } mist.dynAddStatic(cargoTable) -- smoke the cargo if needed if cargoSmoke then local smokePosition={x=spawnPosition.x + mist.random(10,20), y=0, z=spawnPosition.y + mist.random(10,20)} local height = veaf.getLandHeight(smokePosition) smokePosition.y = height veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnCargo: smokePosition x=%.1f y=%.1f z=%.1f", smokePosition.x, smokePosition.y, smokePosition.z)) veafSpawn.spawnSmoke(smokePosition, trigger.smokeColor.Green) for i = 1, 10 do veaf.loggers.get(veafSpawn.Id):trace("Signal flare 1 at " .. timer.getTime() + i*7) mist.scheduleFunction(veafSpawn.spawnSignalFlare, {smokePosition, nil, nil, trigger.flareColor.Red}, timer.getTime() + i*3) end end -- message the unit spawning local message = "Cargo " .. unitName .. " weighting " .. cargoWeight .. " kg has been spawned" if cargoSmoke then message = message .. ". It's marked with green smoke and red flares" end if not(silent) then trigger.action.outText(message, 15) end end else veaf.loggers.get(veafSpawn.Id):info("could not find cargo type named ".. veaf.p(cargoType)) trigger.action.outText("could not find cargo type named ".. veaf.p(cargoType), 15) return end return unitName end --- Spawn a specific static at a specific spot function veafSpawn.doSpawnStatic(spawnSpot, radius, staticCategory, staticType, country, unitName, smoke, silent, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("doSpawnStatic(staticCategory = " .. staticCategory ..")") veaf.loggers.get(veafSpawn.Id):debug("doSpawnStatic(staticType = " .. staticType ..")") local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace(string.format("doSpawnStatic: spawnSpot x=%.1f y=%.1f, z=%.1f", spawnSpot.x, spawnSpot.y, spawnSpot.z)) local units = {} local spawnPosition = veaf.findPointInZone(spawnSpot, 50, false) -- check spawned position validity if spawnPosition == nil then veaf.loggers.get(veafSpawn.Id):info("cannot find a suitable position for spawning static "..staticType) if not(silent) then trigger.action.outText("cannot find a suitable position for spawning static "..staticType, 5) end return end veaf.loggers.get(veafSpawn.Id):trace(string.format("doSpawnStatic: spawnPosition x=%.1f y=%.1f", spawnPosition.x, spawnPosition.y)) local unit = veafUnits.findDcsUnit(staticType) if unit then if not(unitName) then veafSpawn.spawnedUnitsCounter = veafSpawn.spawnedUnitsCounter + 1 unitName = unit.name .. " #" .. veafSpawn.spawnedUnitsCounter end -- create the static local staticTable = { category = staticCategory, type = staticType, country = country, name = unitName, x = spawnPosition.x, y = spawnPosition.y, hiddenOnMFD = hiddenOnMFD, } mist.dynAddStatic(staticTable) -- smoke if needed if smoke then local smokePosition={x=spawnPosition.x + mist.random(10,20), y=0, z=spawnPosition.y + mist.random(10,20)} local height = veaf.getLandHeight(smokePosition) smokePosition.y = height veaf.loggers.get(veafSpawn.Id):trace(string.format("doSpawnStatic: smokePosition x=%.1f y=%.1f z=%.1f", smokePosition.x, smokePosition.y, smokePosition.z)) veafSpawn.spawnSmoke(smokePosition, trigger.smokeColor.Green) for i = 1, 10 do veaf.loggers.get(veafSpawn.Id):trace("Signal flare 1 at " .. timer.getTime() + i*7) mist.scheduleFunction(veafSpawn.spawnSignalFlare, {smokePosition, nil, nil, trigger.flareColor.Red}, timer.getTime() + i*3) end end -- message the unit spawning local message = "Static " .. unitName .. " has been spawned" if smoke then message = message .. ". It's marked with green smoke and red flares" end if not(silent) then trigger.action.outText(message, 5) end end return unitName end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Smoke and Flare commands ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- trigger an explosion at the marker area function veafSpawn.spawnBomb(spawnSpot, radius, shells, power, altitude, altitudedelta, password) veaf.loggers.get(veafSpawn.Id):debug("spawnBomb(power=" .. power ..")") local shellTime = 0 local shellDelay = 0 for shell=1,shells do local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s", spawnSpot) veaf.loggers.get(veafSpawn.Id):trace("altitude=%s", altitude) if altitude and altitude > 0 then spawnSpot.y = altitude + altitudedelta * ((math.random(100)-50)/100) shellDelay = veafSpawn.FlakingInterval else shellDelay = veafSpawn.ShellingInterval end veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s", spawnSpot) local shellDelay = shellDelay * (math.random(100) + 30)/100 local shellPower = power * (math.random(100) + 30)/100 -- check security if not veafSecurity.checkPassword_L0(password) then if shellPower > 1000 then shellPower = 1000 end end veaf.loggers.get(veafSpawn.Id):trace(string.format("shell #%d : shellTime=%d, shellDelay=%d, power=%d", shell, shellTime, shellDelay, shellPower)) mist.scheduleFunction(trigger.action.explosion, {spawnSpot, power}, timer.getTime() + shellTime) shellTime = shellTime + shellDelay end end --- add a smoke marker over the marker area function veafSpawn.spawnSmoke(spawnSpot, color, radius, shells) veaf.loggers.get(veafSpawn.Id):debug("spawnSmoke(color=%s",veaf.p(color)) local radius = radius or 50 local shells = shells or 1 veaf.loggers.get(veafSpawn.Id):trace("radius=%s", veaf.p(radius)) veaf.loggers.get(veafSpawn.Id):trace("shells=%s", veaf.p(shells)) local shellTime = 0 for shell=1,shells do local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnSpot=%s", veaf.vecToString(spawnSpot))) local shellDelay = veafSpawn.ShellingInterval * (math.random(100) + 30)/100 veaf.loggers.get(veafSpawn.Id):trace(string.format("shell #%d : shellTime=%d, shellDelay=%d", shell, shellTime, shellDelay)) if shells > 1 then -- add a small explosion under the smoke to simulate smoke shells mist.scheduleFunction(trigger.action.explosion, {spawnSpot, 1}, timer.getTime() + shellTime-1) end mist.scheduleFunction(trigger.action.smoke, {spawnSpot, color}, timer.getTime() + shellTime) shellTime = shellTime + shellDelay end end --- add a signal flare over the marker area function veafSpawn.spawnSignalFlare(spawnSpot, radius, shells, color) veaf.loggers.get(veafSpawn.Id):debug("spawnSignalFlare(color = " .. color ..")") local shellTime = 0 for shell=1,shells do local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnSpot=%s", veaf.vecToString(spawnSpot))) local shellDelay = veafSpawn.ShellingInterval * (math.random(100) + 30)/100 local azimuth = math.random(359) veaf.loggers.get(veafSpawn.Id):trace(string.format("shell #%d : shellTime=%d, shellDelay=%d", shell, shellTime, shellDelay)) mist.scheduleFunction(trigger.action.signalFlare, {spawnSpot, color, azimuth}, timer.getTime() + shellTime) shellTime = shellTime + shellDelay end end --- add an illumination flare over the target area function veafSpawn.spawnIlluminationFlare(spawnSpot, radius, steps, power, height, heading, distance, speed) local radius = radius or 50 local steps = steps or 1 local power = power or 10 local height = height or 500 veaf.loggers.get(veafSpawn.Id):debug("spawnIlluminationFlare()") veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s", veaf.p(spawnSpot)) veaf.loggers.get(veafSpawn.Id):trace("radius=%s", veaf.p(radius)) veaf.loggers.get(veafSpawn.Id):trace("steps=%s", veaf.p(steps)) veaf.loggers.get(veafSpawn.Id):trace("power=%s", veaf.p(power)) veaf.loggers.get(veafSpawn.Id):trace("height=%s", veaf.p(height)) veaf.loggers.get(veafSpawn.Id):trace("heading=%s", veaf.p(heading)) veaf.loggers.get(veafSpawn.Id):trace("distance=%s", veaf.p(distance)) local cosHeading local sinHeading local stepDistance if heading then if distance then distance = distance * 1852 -- meters stepDistance = distance / (steps - 1) elseif speed then speed = speed / 1.94384 -- m/s stepDistance = speed * veafSpawn.IlluminationShellingInterval end local headingRad = mist.utils.toRadian(heading) cosHeading = math.cos(headingRad) sinHeading = math.sin(headingRad) end local stepTime = 0 for step=1, steps do local stepDelay = veafSpawn.IlluminationShellingInterval * (math.random(100, 130)-15)/100 local newSpawnSpot = mist.utils.deepCopy(spawnSpot) if stepDistance then newSpawnSpot.x = spawnSpot.x + stepDistance * (step - 1) * cosHeading newSpawnSpot.z = spawnSpot.z + stepDistance * (step - 1) * sinHeading end local shellsPerStep = math.random(5, 10) veaf.loggers.get(veafSpawn.Id):trace(string.format("step #%d : stepTime=%d, shellDelay=%d", step, stepTime, stepDelay)) for shell=1, shellsPerStep do local shellDelay = shell/4 + (math.random(100, 150)-25)/100 local shellHeight = height * (math.random(100, 130)-15)/100 local shellPower = power * (math.random(100, 130)-15)/100 local newSpawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(newSpawnSpot, radius)) newSpawnSpot.y = veaf.getLandHeight(newSpawnSpot) + shellHeight veaf.loggers.get(veafSpawn.Id):trace(string.format("shell #%d : shellHeight=%d, shellPower=%d", shell, shellHeight, shellPower)) local time = timer.getTime() + stepTime + shellDelay -- add a small explosion under the flare to simulate flare shells mist.scheduleFunction(trigger.action.explosion, {newSpawnSpot, 0.1}, time) mist.scheduleFunction(trigger.action.illuminationBomb, {newSpawnSpot, shellPower}, time) end stepTime = stepTime + stepDelay end end --- FLAK-related constants veafSpawn.NB_OF_FLAKS_AT_DENSITY_1 = 30 veafSpawn.DEFAULT_FLAK_CLOUD_SIZE = 30 veafSpawn.DEFAULT_FLAK_POWER = 1 veafSpawn.DEFAULT_FLAK_REPEAT_DELAY = 0.2 veafSpawn.DEFAULT_FLAK_FIRE_DELAY = 0.1 function veafSpawn.destroyObjectWithFlak(object, power, density) veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.destroyObjectWithFlak(%s, %s, %s)", veaf.p(power), veaf.p(power), veaf.p(density))) veaf.loggers.get(veafSpawn.Id):trace(string.format("object=%s", veaf.p(object))) local _power = power or veafSpawn.DEFAULT_FLAK_POWER local _density = density or 1 if object and object:isExist() then local point = object:getPoint() local positionForFlak = mist.vec.add(point, mist.vec.scalarMult(object:getVelocity(), veafSpawn.DEFAULT_FLAK_FIRE_DELAY)) local nbFlaks = veafSpawn.NB_OF_FLAKS_AT_DENSITY_1 * _density veaf.loggers.get(veafSpawn.Id):trace(string.format("firing %d flak shells", nbFlaks)) for i = 1, nbFlaks do local flakPoint = { x = point.x + (veafSpawn.DEFAULT_FLAK_CLOUD_SIZE * math.random(-100,100) / 100), y = point.y + (veafSpawn.DEFAULT_FLAK_CLOUD_SIZE * math.random(-100,100) / 100), z = point.z + (veafSpawn.DEFAULT_FLAK_CLOUD_SIZE * math.random(-100,100) / 100) } --veaf.loggers.get(veafSpawn.Id):trace(string.format("flakPoint=%s", veaf.p(flakPoint))) trigger.action.explosion(flakPoint, _power) end -- reschedule to check if the object is destroyed veaf.loggers.get(veafSpawn.Id):trace(string.format("reschedule to check if the object is destroyed")) mist.scheduleFunction(veafSpawn.destroyObjectWithFlak, {object, power, power, density}, timer.getTime() + veafSpawn.DEFAULT_FLAK_REPEAT_DELAY) end end --- destroy unit(s) function veafSpawn.destroy(spawnSpot, radius, unitName) veaf.loggers.get(veafSpawn.Id):debug(string.format("destroy(radius=%s, unitName=%s)", tostring(radius), tostring(unitName))) veaf.loggers.get(veafSpawn.Id):trace(string.format("spawnSpot=%s", veaf.p(spawnSpot))) if unitName then -- destroy a specific unit local c = Unit.getByName(unitName) if c then veaf.loggers.get(veafSpawn.Id):trace("destroy a specific unit") Unit.destroy(c) end -- or a specific static c = StaticObject.getByName(unitName) if c then veaf.loggers.get(veafSpawn.Id):trace("destroy a specific static") StaticObject.destroy(c) end -- or a specific group c = Group.getByName(unitName) if c then veaf.loggers.get(veafSpawn.Id):trace("destroy a specific group") Group.destroy(c) end else -- radius based destruction veaf.loggers.get(veafSpawn.Id):trace("radius based destruction") local units = veaf.findUnitsInCircle(spawnSpot, radius or 150, true) veaf.loggers.get(veafSpawn.Id):trace(string.format("units=%s", veaf.p(units))) if units then for name, _ in pairs(units) do -- try and find a unit local unit = Unit.getByName(name) if unit then Unit.destroy(unit) else unit = StaticObject.getByName(name) if unit then StaticObject.destroy(unit) end end end end end end --- teleport group function veafSpawn.teleport(spawnSpot, name, silent) veaf.loggers.get(veafSpawn.Id):debug("teleport(name = " .. name ..")") local vars = { groupName = name, point = spawnSpot, action = "teleport" } local grp = mist.teleportToPoint(vars) if not silent then if grp then trigger.action.outText("Teleported group "..name, 5) else trigger.action.outText("Cannot teleport group : "..name, 5) end end end function veafSpawn._findClosestConvoy(unitName) veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn._findClosestConvoy(%s)",unitName)) local closestConvoyName = nil local minDistance = 99999999 local unit = veafRadio.getHumanUnitOrWingman(unitName) if unit then for name, _ in pairs(veafSpawn.spawnedConvoys) do local averageGroupPosition = veaf.getAveragePosition(name) if not averageGroupPosition then veaf.loggers.get(veafSpawn.Id):error("cannot get average position of %s",veaf.p(unitName)) return nil end local distanceFromPlayer = ((averageGroupPosition.x - unit:getPosition().p.x)^2 + (averageGroupPosition.z - unit:getPosition().p.z)^2)^0.5 veaf.loggers.get(veafSpawn.Id):trace(string.format("distanceFromPlayer = %d",distanceFromPlayer)) if distanceFromPlayer < minDistance then minDistance = distanceFromPlayer closestConvoyName = name veaf.loggers.get(veafSpawn.Id):trace(string.format("convoy %s is closest",closestConvoyName)) end end end return closestConvoyName end function veafSpawn._commandConvoy(convoyName, stop) local group = Group.getByName(convoyName) if group then if stop then local stopped = veafSpawn.spawnedConvoys[convoyName].stopped if stopped then -- already stopped ! return false else local task ={ id = 'Hold', params = { } } group:getController():pushTask(task) veafSpawn.spawnedConvoys[convoyName].stopped = true end else local stopped = veafSpawn.spawnedConvoys[convoyName].stopped if stopped then mist.goRoute(convoyName, veafSpawn.spawnedConvoys[convoyName].route) veafSpawn.spawnedConvoys[convoyName].stopped = false else -- not stopped ! return false end end end end function veafSpawn.stopClosestConvoy(unitName) veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.stopClosestConvoy(unitName=%s)",unitName)) local convoyName = veafSpawn._findClosestConvoy(unitName) if convoyName then return veafSpawn._commandConvoy(convoyName, true) end end function veafSpawn.moveClosestConvoy(unitName) veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.moveClosestConvoy(unitName=%s)",unitName)) local convoyName = veafSpawn._findClosestConvoy(unitName) if convoyName then return veafSpawn._commandConvoy(convoyName, false) end end function veafSpawn._markClosestConvoyWithSmoke(unitName, markRoute) veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.markClosestConvoyWithSmoke(unitName=%s)",unitName)) local closestConvoyName = veafSpawn._findClosestConvoy(unitName) if closestConvoyName then if markRoute then local route = veafSpawn.spawnedConvoys[closestConvoyName].route local startPoint = veaf.placePointOnLand({x = route[1].x, y = 0, z = route[1].y}) local endPoint = veaf.placePointOnLand({x = route[2].x, y = 0, z = route[2].y}) trigger.action.smoke(startPoint, trigger.smokeColor.Green) trigger.action.smoke(endPoint, trigger.smokeColor.Red) veaf.outTextForUnit(unitName, closestConvoyName .. " is going from green to red smoke", 10) else local averageGroupPosition = veaf.getAveragePosition(closestConvoyName) trigger.action.smoke(averageGroupPosition, trigger.smokeColor.White) veaf.outTextForUnit(unitName, closestConvoyName .. " marked with white smoke", 10) end else veaf.outTextForUnit(unitName, "No convoy found", 10) end end function veafSpawn.markClosestConvoyWithSmoke(unitName) return veafSpawn._markClosestConvoyWithSmoke(unitName, false) end function veafSpawn.markClosestConvoyRouteWithSmoke(unitName) return veafSpawn._markClosestConvoyWithSmoke(unitName, true) end function veafSpawn.infoOnAllConvoys(unitName) veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.infoOnAllConvoys(unitName=%s)",unitName)) local text = "" for name, _ in pairs(veafSpawn.spawnedConvoys) do local nbVehicles, nbInfantry = veafUnits.countInfantryAndVehicles(name) if nbVehicles > 0 then local averageGroupPosition = veaf.getAveragePosition(name) local lat, lon = coord.LOtoLL(averageGroupPosition) local llString = mist.tostringLL(lat, lon, 0, true) text = text .. " - " .. name .. ", " .. nbVehicles .. " vehicles : " .. llString if veafSpawn.spawnedConvoys[name].stopped then text = text .. ", stopped" end else text = text .. " - " .. name .. "has been destroyed" -- convoy has been dispatched, remove it from the convoys list veafSpawn.spawnedConvoys[name] = nil end end if text == "" then veaf.outTextForUnit(unitName, "No convoy found", 10) else veaf.outTextForUnit(unitName, text, 30) end end function veafSpawn.cleanupAllConvoys() veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.cleanupAllConvoys()") local foundOne = false for name, _ in pairs(veafSpawn.spawnedConvoys) do foundOne = true local nbVehicles, nbInfantry = veafUnits.countInfantryAndVehicles(name) if nbVehicles > 0 then local group = Group.getByName(name) if group then Group.destroy(group) end end -- convoy has been dispatched, remove it from the convoys list veafSpawn.spawnedConvoys[name] = nil end if foundOne then trigger.action.outText("All convoys cleaned up", 10) else trigger.action.outText("No convoy found", 10) end end function veafSpawn.JTACAutoLase(groupName, laserCode, radioData) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.JTACAutoLase()") veaf.loggers.get(veafSpawn.Id):trace(string.format("groupName=%s",tostring(groupName))) veaf.loggers.get(veafSpawn.Id):trace(string.format("laserCode=%s",tostring(laserCode))) veaf.loggers.get(veafSpawn.Id):trace(string.format("radioData=%s\n",veaf.p(radioData))) local _radio = radioData or {} veaf.loggers.get(veafSpawn.Id):trace(string.format("_radio=%s\n",veaf.p(_radio))) veaf.loggers.get(veafSpawn.Id):trace(string.format("calling CTLD")) ctld.JTACAutoLase(groupName, laserCode, false, "all", nil, _radio) veaf.loggers.get(veafSpawn.Id):trace(string.format("CTLD called")) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- air units templates ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafAirUnitTemplate object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafAirUnitTemplate = {} function VeafAirUnitTemplate:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object -- name objectToCreate.name = nil -- coalition (0 = neutral, 1 = red, 2 = blue) objectToCreate.coalition = nil -- route, only for veaf commands (groups already have theirs) objectToCreate.route = nil objectToCreate.humanName = nil objectToCreate.groupData = nil return objectToCreate end --- --- setters and getters --- function VeafAirUnitTemplate:setName(value) self.name = value return self end function VeafAirUnitTemplate:getName() return self.name end function VeafAirUnitTemplate:setCoalition(value) self.coalition = value return self end function VeafAirUnitTemplate:getCoalition() return self.coalition end function VeafAirUnitTemplate:setGroupData(value) self.groupData = value return self end function VeafAirUnitTemplate:getGroupData() return self.groupData end function veafSpawn.initializeAirUnitTemplates() veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.initializeAirUnitTemplates()") -- find groups with the air units template prefix veaf.loggers.get(veafSpawn.Id):debug("find groups with the air units template prefix") local _prefix = veafSpawn.AirUnitTemplatesPrefix:upper() veaf.loggers.get(veafSpawn.Id):trace("_prefix=%s",_prefix) local _templateGroups = {} local _groups = veaf.getGroupsOfCoalition() for _, group in pairs(_groups) do local _name = group:getName():upper() --veaf.loggers.get(veafSpawn.Id):trace("_name=%s",_name) if string.sub(_name,1,string.len(_prefix)) == _prefix then table.insert(_templateGroups, group) end end veaf.loggers.get(veafSpawn.Id):trace("_templateGroups=%s", _templateGroups) for _, group in pairs(_templateGroups) do local _groupName = group:getName() veaf.loggers.get(veafSpawn.Id):trace("_groupName=%s", _groupName) local _template = VeafAirUnitTemplate:new():setName(_groupName) veafSpawn.airUnitTemplates[_groupName:upper()] = _template end -- find groups within the veafSpawn.SpawnablePlanes table -- DOES NOT WORK YET if veafSpawn.SpawnablePlanes then veaf.loggers.get(veafSpawn.Id):debug("find groups within the veafSpawn.SpawnablePlanes table") for _, groupData in pairs(veafSpawn.SpawnablePlanes) do local _groupName = groupData.name veaf.loggers.get(veafSpawn.Id):trace("_groupName=%s", _groupName) groupData.country="russia" groupData.countryId=0 groupData.category="plane" groupData.coalition="red" groupData.uncontrolled=false groupData.hidden=false local _template = VeafAirUnitTemplate:new():setName(_groupName):setGroupData(groupData) veafSpawn.airUnitTemplates[_groupName:upper()] = _template end end end function veafSpawn.listAllCAP(unitName) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.listAllCAP(unitName=%s)",unitName) local sorted = {} for name, template in pairs(veafSpawn.airUnitTemplates) do local _name = template:getName():sub(veafSpawn.AirUnitTemplatesPrefix:len()+1) table.insert(sorted, _name) end table.sort(sorted) local text = "" for _, name in pairs(sorted) do text = text .. name .. "\n" end if text == "" then veaf.outTextForUnit(unitName, "No CAP available for spawn", 10) else veaf.outTextForUnit(unitName, text, 30) end end function veafSpawn.dumpSpawnablePlanesList(export_path) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.dumpSpawnablePlanesList(export_path=%s)", export_path) local jsonify = function(key, value) veaf.loggers.get(veafSpawn.Id):trace(string.format("jsonify(%s)", veaf.p(value))) if veaf.json then return veaf.json.stringify(value) else return "" end end -- sort the spawnable planes alphabetically local sortedSpawnablePlanesNames = {} for _, spawnablePlane in pairs(veafSpawn.airUnitTemplates) do local _name = spawnablePlane:getName():sub(veafSpawn.AirUnitTemplatesPrefix:len()+1) table.insert(sortedSpawnablePlanesNames, _name) end table.sort(sortedSpawnablePlanesNames) veaf.loggers.get(veafSpawn.Id):trace("sortedSpawnablePlanesNames=%s", veaf.p(sortedSpawnablePlanesNames)) local _filename = "SpawnablePlanes.json" if veaf.config.MISSION_NAME then _filename = "SpawnablePlanesList_" .. veaf.config.MISSION_NAME .. ".json" end if not(veaf.DO_NOT_EXPORT_JSON_FILES) then veaf.exportAsJson(sortedSpawnablePlanesNames, "spawnablePlanes", jsonify, _filename, export_path or veaf.config.MISSION_EXPORT_PATH) end end function veafSpawn.spawnAFAC(spawnSpot, name, country, altitude, speed, hdg, frequency, mod, code, immortal, silent, hiddenOnMFD) local coalition = veaf.getCoalitionForCountry(country, true) if not coalition then veaf.loggers.get(veafSpawn.Id):error("No country/coalition for AFAC !") return nil end -- find template amongst the existing templates (name can be a regex) local groupName = veafSpawn.findSpawnableAircraftGroupname(name) if not groupName then local message = string.format("The AFAC aircraft template could not be found for \"%s\"", veaf.p(name)) veaf.loggers.get(veafSpawn.Id):info(message) trigger.action.outTextForCoalition(coalition, message, 15) return nil end veaf.loggers.get(veafSpawn.Id):trace("found template=%s",groupName) if not veafSpawn.AFAC.numberSpawned[coalition] then veafSpawn.AFAC.numberSpawned[coalition] = 1 elseif veafSpawn.AFAC.numberSpawned[coalition] > veafSpawn.AFAC.maximumAmount then veaf.loggers.get(veafSpawn.Id):info("The limit for AFACs was reached, one needs to be destroyed") if not silent then trigger.action.outTextForCoalition(coalition, "The limit for AFACs was reached, one needs to be destroyed", 15) end return false end veaf.loggers.get(veafSpawn.Id):info(string.format("number of AFAC spawned : %s", veaf.p(veafSpawn.AFAC.numberSpawned[coalition]))) local AFAC_num = veafSpawn.AFAC.numberSpawned[coalition] local newGroupName = veafSpawn.AFAC.callsigns[coalition][AFAC_num].name for i = 1, veafSpawn.AFAC.maximumAmount do if veafSpawn.AFAC.callsigns[coalition][i].taken == false then newGroupName = veafSpawn.AFAC.callsigns[coalition][i].name AFAC_num = i break end end veaf.loggers.get(veafSpawn.Id):trace("newGroupName=%s",newGroupName) veaf.loggers.get(veafSpawn.Id):trace("AFAC_num=%s",AFAC_num) veaf.loggers.get(veafSpawn.Id):trace("AFAC coalition=%s",coalition) --essentially the same counter but for the template group itself, not for all AFACs if not veafSpawn.spawnedNamesIndex[groupName] then veafSpawn.spawnedNamesIndex[groupName] = 1 end local codeDigit = {} codeDigit = veaf.laserCodeToDigit(code) local altitude = altitude or 15000 if altitude <= 8000 then altitude = 15000 -- ft end local speed = speed or 150 -- kn -- convert speed to m/s speed = speed/1.94384 -- convert altitude to meters altitude = altitude * 0.3048 -- meters --convert heading to radians if hdg then hdg = hdg * math.pi / 180 else hdg = 0 end local distanceFromTeleport = 3000 --distance between the orbit point and the teleport point in meters --calculate DCS radio frequency based on which AFAC out of 8 this is local dcsFrequency = veafSpawn.AFAC.baseAFACfrequency[coalition]+(AFAC_num-1)*50000 -- .05 MHz increments veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s", veaf.p(spawnSpot)) veaf.loggers.get(veafSpawn.Id):trace("name=%s", veaf.p(name)) veaf.loggers.get(veafSpawn.Id):trace("country=%s", veaf.p(country)) veaf.loggers.get(veafSpawn.Id):trace("altitude (m)=%s", veaf.p(altitude)) veaf.loggers.get(veafSpawn.Id):trace("speed (m/s)=%s", veaf.p(speed)) veaf.loggers.get(veafSpawn.Id):trace("frequency=%s", veaf.p(frequency)) veaf.loggers.get(veafSpawn.Id):trace("dcsFrequency=%s", veaf.p(dcsFrequency)) veaf.loggers.get(veafSpawn.Id):trace("code=%s", veaf.p(code)) veaf.loggers.get(veafSpawn.Id):trace("mod=%s", veaf.p(mod)) veaf.loggers.get(veafSpawn.Id):trace("silent=%s", veaf.p(silent)) veaf.loggers.get(veafSpawn.Id):trace("hiddenOnMFD=%s", veaf.p(hiddenOnMFD)) local teleportSpot = {} teleportSpot.x = spawnSpot.x - distanceFromTeleport*math.cos(hdg) --teleport spot is 3km south of the orbit point teleportSpot.y = spawnSpot.z - distanceFromTeleport*math.sin(hdg) teleportSpot.alt = altitude teleportSpot.speed = speed --define 2 point route + teleport Waypoint local WP = {} WP.one = {} WP.two = {} WP.three = {} WP.one.x = teleportSpot.x WP.one.y = teleportSpot.y WP.two.x = spawnSpot.x - distanceFromTeleport*math.cos(hdg)/2 WP.two.y = spawnSpot.z - distanceFromTeleport*math.sin(hdg)/2 WP.three.x = spawnSpot.x WP.three.y = spawnSpot.z veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "AFAC", "teleportPoint", WP.one) veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "AFAC", "setupPoint", WP.two) veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "AFAC", "orbitPoint", WP.three) local newRoute = { ["points"] = { -- first point [1] = { ["type"] = "Turning Point", ["action"] = "Turning Point", ["x"] = WP.two.x, --1500m south of the orbit point ["y"] = WP.two.y, ["alt"] = altitude, -- in meters ["alt_type"] = "BARO", ["speed"] = speed, -- speed in m/s ["speed_locked"] = true, ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { [1] = { ["id"]="FAC", ["params"] = { ["frequency"]=dcsFrequency, ["modulation"]=0, --0 is AM, 1 is FM ["callname"]=AFAC_num, ["number"]=7+coalition, --number x as in it's callsign Springfield x-1 for example ["priority"]=0, } } -- end of [1] } -- end of tasks } -- end of params } -- end of task }, -- end of waypoint 1 [2] = { ["type"] = "Turning Point", ["action"] = "Turning Point", ["x"] = WP.three.x, ["y"] = WP.three.y, ["alt"] = altitude, -- in meters ["alt_type"] = "BARO", ["speed"] = speed, -- speed in m/s ["speed_locked"] = true, ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { [1] = { ["id"] = "Orbit", ["params"] = { ["altitude"] = altitude, -- in meters, ["pattern"] = "Circle", ["speed"] = speed, -- speed in m/s }, -- end of ["params"] } -- end of [1] } -- end of ["tasks"] } -- end of ["params"] } -- end of ["task"] } -- end of waypoint 2 } } -- (re)spawn group local vars = {} vars.gpName = groupName vars.name = groupName --vars.groupData = _template:getGroupData() --replace the callsign to prevent interractions vars.route = newRoute vars.action = 'clone' vars.point = teleportSpot vars.newGroupName = newGroupName local newGroup = mist.teleportToPoint(vars, true) if not newGroup then veaf.loggers.get(veafSpawn.Id):error("cannot respawn group %s",veaf.p(vars.name)) return nil end if country and #country > 0 then newGroup.coalition = coalition newGroup.countryId = veaf.getCountryId(country) end --newGroup.task = "AFAC" veaf.loggers.get(veafSpawn.Id):trace("newGroup=%s", veaf.p(newGroup, nil, {"route", "payload"})) --setup of the new group local unit = newGroup.units[1] if not unit then veaf.loggers.get(veafSpawn.Id):error("cannot get first unit of group %s",veaf.p(newGroup:getName())) return nil end unit.skill = "Excellent" newGroup.hidden=false newGroup.name = newGroupName newGroup.hiddenOnMFD = hiddenOnMFD local unitName = newGroupName veaf.loggers.get(veafSpawn.Id):trace("unitName=%s",unitName) unit.unitName = unitName unit.name = unitName newGroup.sameName = true unit.alt = teleportSpot.alt veaf.loggers.get(veafSpawn.Id):trace("newGroup=%s", veaf.p(newGroup, nil, {"route", "payload"})) local _spawnedGroup = mist.dynAdd(newGroup) if _spawnedGroup then veaf.loggers.get(veafSpawn.Id):trace("_spawnedGroup=%s", veaf.p(_spawnedGroup, nil, {"route", "payload"})) veaf.loggers.get(veafSpawn.Id):trace("_spawnedGroup.name=%s",_spawnedGroup.name) --mist.goRoute(_spawnedGroup.name, newRoute) _spawnedGroup.category = "AIRPLANE" _spawnedGroup.country = country veaf.loggers.get(veafSpawn.Id):trace("_spawnedGroup=%s", veaf.p(_spawnedGroup)) veafSpawn.AFAC.missionData[coalition][AFAC_num] = _spawnedGroup --since MIST does not store cloned group data, this is a bit of trickery to allow teleporting AFACs -- start lasing if ctld then ctld.cleanupJTAC(_spawnedGroup.name) local radioData = {freq=frequency, mod=mod, name=_spawnedGroup.name} veafSpawn.JTACAutoLase(_spawnedGroup.name, code, radioData) end local humanFrequency = dcsFrequency/1000000 local text = "AFAC " .. string.format(veafSpawn.AFAC.numberSpawned[coalition]) .. "/" .. string.format(veafSpawn.AFAC.maximumAmount) .. " - " .. string.format(_spawnedGroup.name) .. " (" .. string.format(country) .. ") - on " .. string.format(humanFrequency) .. "AM (DCS AFAC) or " .. string.format(frequency) .. string.upper(mod) .. " (SRS)" veaf.loggers.get(veafSpawn.Id):info(text) if not silent then trigger.action.outTextForCoalition(coalition, text, 15) end local _dcsSpawnedGroup = Group.getByName(_spawnedGroup.name) local controller = _dcsSpawnedGroup:getController() if immortal then veaf.loggers.get(veafSpawn.Id):trace("AFAC immortalized") -- JTAC needs to be invisible and immortal local _setImmortal = { id = 'SetImmortal', params = { value = true } } -- invisible to AI, Shagrat local _setInvisible = { id = 'SetInvisible', params = { value = true } } Controller.setCommand(controller, _setImmortal) Controller.setCommand(controller, _setInvisible) end --set the callsign to avoid desyncs in the DCS JTAC menu local _setCallsign = { id = 'SetCallsign', params = { callname = AFAC_num, number = 9, } } Controller.setCommand(controller, _setCallsign) if veafNamedPoints and not silent then text = "AFAC" .. " - " .. string.format(_spawnedGroup.name) .. " - " .. string.format(humanFrequency) .. "AM (DCS) or " .. string.format(frequency) .. string.upper(mod) .. " (SRS)" veafNamedPoints.namePoint({x=spawnSpot.x, y=altitude, z=spawnSpot.z}, text, veaf.getCoalitionForCountry(country, true), true) end veafSpawn.afacWatchdog(newGroupName, AFAC_num, coalition, text) veafSpawn.AFAC.callsigns[coalition][AFAC_num].taken = true veafSpawn.spawnedNamesIndex[groupName] = veafSpawn.spawnedNamesIndex[groupName] + 1 veafSpawn.AFAC.numberSpawned[coalition] = veafSpawn.AFAC.numberSpawned[coalition] + 1 return _spawnedGroup.name else veaf.loggers.get(veafSpawn.Id):error("MIST could not add AFAC") return nil end end function veafSpawn.afacWatchdog(afacGroupName, AFAC_num, coalition, markName) if afacGroupName and not Group.getByName(afacGroupName) then veaf.loggers.get(veafSpawn.Id):info(string.format("AFAC named=%s is KIA, removing mark (if it exists) and allowing it to be spawned again", veaf.p(afacGroupName))) veaf.loggers.get(veafSpawn.Id):trace(string.format("markName=%s", veaf.p(markName))) if veafNamedPoints and markName then local existingPoint = veafNamedPoints.getPoint(markName) veaf.loggers.get(veafSpawn.Id):trace(string.format("existingPoint=%s", veaf.p(existingPoint))) if existingPoint and existingPoint.markerId then -- delete the existing point trigger.action.removeMark(existingPoint.markerId) end end --Make the callsign index available again for spawn veaf.loggers.get(veafSpawn.Id):trace(string.format("AFAC_num=%s", veaf.p(AFAC_num))) veafSpawn.AFAC.callsigns[coalition][AFAC_num].taken = false veafSpawn.AFAC.numberSpawned[coalition] = veafSpawn.AFAC.numberSpawned[coalition] - 1 mist.DBs.unitsByName[afacGroupName] = nil --MIST does not do it on it's own, I highly recommend looking for an alternative, this is to spawn the AFAC once again with the unit name equal to the group name mist.DBs.groupsByName[afacGroupName] = nil veafSpawn.AFAC.missionData[coalition][AFAC_num] = nil else veaf.loggers.get(veafSpawn.Id):trace(string.format("AFAC named=%s is alive", veaf.p(afacGroupName))) --update the mark if the AFAC moves if veafNamedPoints and markName then local existingPoint = veafNamedPoints.getPoint(markName) veaf.loggers.get(veafSpawn.Id):trace(string.format("existingAFACmarker=%s", veaf.p(existingPoint))) if existingPoint and existingPoint.markerId then local AFAC_points = veafSpawn.AFAC.missionData[coalition][AFAC_num].route.points local orbitPoint = AFAC_points[#AFAC_points] if existingPoint.x ~= orbitPoint.x and existingPoint.z ~= orbitPoint.y then -- delete the existing point veaf.loggers.get(veafSpawn.Id):trace(string.format("Marker needs updating, AFAC moved, newAFACmarker=%s", veaf.p(orbitPoint))) trigger.action.removeMark(existingPoint.markerId) veafNamedPoints.namePoint({x=orbitPoint.x, y=orbitPoint.alt, z=orbitPoint.y}, markName, coalition, true) end end end mist.scheduleFunction(veafSpawn.afacWatchdog, {afacGroupName, AFAC_num, coalition, markName}, timer.getTime()+120) end end function veafSpawn.findSpawnableAircraftGroupname(name) -- find template amongst the existing templates (name can be a regex) local nameUpper = (name or ""):upper() local regexNameUpper = ".*" .. (nameUpper or ".*") .. ".*" if not name then regexNameUpper = ".*" end local escapedNameUpper = veaf.escapeRegex(nameUpper) veaf.loggers.get(veafSpawn.Id):trace("nameUpper=%s", veaf.p(nameUpper)) local templatesNamesToChooseFrom = {} local chosenTemplateName = nil for templateNameUpper, templateData in pairs(veafSpawn.airUnitTemplates) do veaf.loggers.get(veafSpawn.Id):trace("templateNameUpper=%s", veaf.p(templateNameUpper)) if templateNameUpper:match(regexNameUpper) or templateNameUpper:match(escapedNameUpper) then local templateName = templateData.name veaf.loggers.get(veafSpawn.Id):trace("templateName=%s", veaf.p(templateName)) table.insert(templatesNamesToChooseFrom, templateName) end end if templatesNamesToChooseFrom and #templatesNamesToChooseFrom > 0 then chosenTemplateName = veaf.randomlyChooseFrom(templatesNamesToChooseFrom) else local message = string.format("The CAP aircraft template could not be found for \"%s\"", veaf.p(name)) veaf.loggers.get(veafSpawn.Id):info(message) trigger.action.outText(message, 15) return nil end veaf.loggers.get(veafSpawn.Id):trace("templatesNamesToChooseFrom=%s", veaf.p(templatesNamesToChooseFrom)) local chosenTemplateData = veaf.getGroupData(chosenTemplateName) veaf.loggers.get(veafSpawn.Id):trace("found template=%s",chosenTemplateData) return chosenTemplateName, chosenTemplateData end function veafSpawn.spawnCombatAirPatrol(spawnSpot, radius, name, country, altitude, altitudeDelta, hdg, distance, speed, capRadius, skill, silent, hiddenOnMFD) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.spawnCombatAirPatrol(name=%s)", name) -- for compatibility reasons we still have altitude and altitudedelta set to zero by default if altitude == 0 then altitude = nil end if altitudeDelta == 0 then altitudeDelta = nil end local coalition = veaf.getCoalitionForCountry(country, true) if not coalition then veaf.loggers.get(veafSpawn.Id):error("No country/coalition for CAP !") return nil end -- find template amongst the existing templates (name can be a regex) local chosenTemplateName, chosenTemplateData = veafSpawn.findSpawnableAircraftGroupname(name) if not chosenTemplateName or not chosenTemplateData then veaf.loggers.get(veafSpawn.Id):error("spawnCombatAirPatrol: could not find a template for %s", veaf.p(name)) return end local function convertSpeeds(speed, mach, altitude) local result = speed if not result then -- compute ground speed in m/s based on MACH and altitude result = veaf.convertMachSpeed(0.3, altitude).TAS_ms else -- compute ground speed in m/s based on IAS and altitude result = veaf.convertIndicatedAirSpeed(speed, altitude).TAS_ms end return result end local radius = radius or 5000 -- m local altitude = (altitude or 27000) --[[ ft ]] * 0.3048 --[[ meters ]] local altitudeDelta = (altitudeDelta or 2000) --[[ ft ]] * 0.3048 --[[ meters ]] local hdg = hdg or 0 local speed0 = convertSpeeds(speed, 0.3, altitude) local speed1 = convertSpeeds(speed, 0.5, altitude) local speed2 = convertSpeeds(speed, 0.63, altitude) local speed3 = convertSpeeds(speed, 0.63, altitude) local distance = (distance or 20) --[[ nm ]] * 1852 --[[ meters ]] local capRadius = (capRadius or 60) * 1852 --[[ meters ]] local skill = skill or "random" veaf.loggers.get(veafSpawn.Id):trace("spawnSpot=%s", veaf.p(spawnSpot)) veaf.loggers.get(veafSpawn.Id):trace("radius=%s", veaf.p(radius)) veaf.loggers.get(veafSpawn.Id):trace("name=%s", veaf.p(name)) veaf.loggers.get(veafSpawn.Id):trace("country=%s", veaf.p(country)) veaf.loggers.get(veafSpawn.Id):trace("altitude=%s", veaf.p(altitude)) veaf.loggers.get(veafSpawn.Id):trace("altdelta=%s", veaf.p(altitudeDelta)) veaf.loggers.get(veafSpawn.Id):trace("hdg=%s", veaf.p(hdg)) veaf.loggers.get(veafSpawn.Id):trace("distance=%s", veaf.p(distance)) veaf.loggers.get(veafSpawn.Id):trace("speed0=%s", veaf.p(speed0)) veaf.loggers.get(veafSpawn.Id):trace("speed1=%s", veaf.p(speed1)) veaf.loggers.get(veafSpawn.Id):trace("speed2=%s", veaf.p(speed2)) veaf.loggers.get(veafSpawn.Id):trace("speed3=%s", veaf.p(speed3)) veaf.loggers.get(veafSpawn.Id):trace("capRadius=%s", veaf.p(capRadius)) veaf.loggers.get(veafSpawn.Id):trace("skill=%s", veaf.p(skill)) veaf.loggers.get(veafSpawn.Id):trace("silent=%s", veaf.p(silent)) veaf.loggers.get(veafSpawn.Id):trace("hiddenOnMFD=%s", veaf.p(hiddenOnMFD)) local getRoute = function(parameters) local newRoute = { ["points"] = { [1] = { ["alt"] = parameters.altitude, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = parameters.speed1, ["properties"] = { ["addopt"] = { }, -- end of ["addopt"] }, -- end of ["properties"] ["task"] = parameters.chosenTemplateWp1Task, ["type"] = "Turning Point", ["ETA"] = 10000, ["ETA_locked"] = false, ["y"] = parameters.wp1.y, ["x"] = parameters.wp1.x, ["formation_template"] = "", ["speed_locked"] = true, }, -- end of [1] [2] = { ["alt"] = parameters.altitude, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = parameters.speed2, ["properties"] = { ["addopt"] = { }, -- end of ["addopt"] }, -- end of ["properties"] ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { -- [1] = -- { -- ["number"] = 1, -- ["auto"] = false, -- ["enabled"] = true, -- ["id"] = "EngageTargetsInZone", -- ["params"] = { -- ["noTargetTypes"] = { -- [1] = "Cruise missiles", -- [2] = "Antiship Missiles", -- [3] = "AA Missiles", -- [4] = "AG Missiles", -- [5] = "SA Missiles", -- }, -- end of ["noTargetTypes"] -- ["priority"] = 0, -- ["targetTypes"] = { -- [1] = "Air", -- }, -- end of ["targetTypes"] -- ["value"] = "Air;", -- ["x"] = parameters.targetZone.x, -- ["y"] = parameters.targetZone.y, -- ["zoneRadius"] = parameters.targetZone.radius, -- }, -- end of ["params"] -- }, -- end of [1] }, -- end of ["tasks"] }, -- end of ["params"] }, -- end of ["task"] ["type"] = "Turning Point", ["ETA"] = 20000, ["ETA_locked"] = false, ["y"] = parameters.wp2.y, ["x"] = parameters.wp2.x, ["formation_template"] = "", ["speed_locked"] = true, }, -- end of [2] [3] = { ["alt"] = parameters.altitude, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = parameters.speed3, ["properties"] = { ["addopt"] = { }, -- end of ["addopt"] }, -- end of ["properties"] ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { [1] = { ["enabled"] = true, ["auto"] = false, ["id"] = "WrappedAction", ["number"] = 1, ["params"] = { ["action"] = { ["id"] = "SwitchWaypoint", ["params"] = { ["goToWaypointIndex"] = 2, ["fromWaypointIndex"] = 3, }, -- end of ["params"] }, -- end of ["action"] }, -- end of ["params"] }, -- end of [1] }, -- end of ["tasks"] }, -- end of ["params"] }, -- end of ["task"] ["type"] = "Turning Point", ["ETA"] = 30000, ["ETA_locked"] = false, ["y"] = parameters.wp3.y, ["x"] = parameters.wp3.x, ["formation_template"] = "", ["speed_locked"] = true, }, -- end of [3] } } return newRoute end -- find spawn spot if altitudeDelta then altitude = altitude + math.random(0, altitudeDelta*2) - altitudeDelta end local position = mist.getRandPointInCircle(spawnSpot, radius) position.z = position.y position.y = altitude veaf.loggers.get(veafSpawn.Id):debug("final spawn, position=%s",position) -- get the template first waypoint's options veaf.loggers.get(veafSpawn.Id):trace("chosenTemplateData=%s", veaf.p(chosenTemplateData)) local chosenTemplateWp1Task = {} if chosenTemplateData then local _route = chosenTemplateData.route --veaf.loggers.get(veafSpawn.Id):trace("_route=%s", veaf.p(_route)) if _route then local _points = _route.points --veaf.loggers.get(veafSpawn.Id):trace("_points=%s", veaf.p(_points)) if _points then local _point1 = _points[1] --veaf.loggers.get(veafSpawn.Id):trace("_point1=%s", veaf.p(_point1)) if _point1 then local _task = _point1.task --veaf.loggers.get(veafSpawn.Id):trace("_task=%s", veaf.p(_task)) if _task and "ComboTask" == _task.id then local _params = _task.params --veaf.loggers.get(veafSpawn.Id):trace("_params=%s", veaf.p(_params)) if _params then local _tasks = _params.tasks --veaf.loggers.get(veafSpawn.Id):trace("_tasks=%s", veaf.p(_tasks)) if _tasks then for _, _taskData in pairs(_tasks) do if "WrappedAction" == _taskData.id then table.insert(chosenTemplateWp1Task, mist.utils.deepCopy(_task)) -- if we found a WrappedAction task then we're on the right way, clone the whole task package break end end end end end end end end end --veaf.loggers.get(veafSpawn.Id):trace("chosenTemplateWp1Task=%s", veaf.p(chosenTemplateWp1Task)) -- compute route local headingRad = mist.utils.toRadian(hdg) local parameters = { altitude = altitude, speed0 = speed0, speed1 = speed1, speed2 = speed2, speed3 = speed3, wp1 = { x = position.x, y = position.z }, wp1Options = chosenTemplateWp1Task } parameters.wp2 = { x = parameters.wp1.x + 2500 * math.cos(headingRad), y = parameters.wp1.y + 2500 * math.sin(headingRad) } -- second wp at 2500m in the right direction parameters.wp3 = { x = parameters.wp2.x + distance * math.cos(headingRad), y = parameters.wp2.y + distance * math.sin(headingRad) } -- last wp at the right distance in the right direction parameters.targetZone = { x = (parameters.wp2.x + parameters.wp3.x) / 2, y = (parameters.wp2.y + parameters.wp3.y) / 2, radius = capRadius } -- target zone at the middle point between wp2 and wp3 veaf.loggers.get(veafSpawn.Id):trace("to create route, parameters=%s",parameters) local newRoute = getRoute(parameters) veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "CAP", "wp1", parameters.wp1) veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "CAP", "wp2", parameters.wp2) veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "CAP", "wp3", parameters.wp3) veafSpawn.traceMarkerId = veaf.loggers.get(veafSpawn.Id):marker(veafSpawn.traceMarkerId, "CAP", "targetZone", parameters.targetZone, nil, capRadius, {1,0,0,0.15}) if not veafSpawn.spawnedNamesIndex[chosenTemplateName] then veafSpawn.spawnedNamesIndex[chosenTemplateName] = 1 else veafSpawn.spawnedNamesIndex[chosenTemplateName] = veafSpawn.spawnedNamesIndex[chosenTemplateName] + 1 end local newGroupName = string.format("%s #%04d", chosenTemplateName, veafSpawn.spawnedNamesIndex[chosenTemplateName]) veaf.loggers.get(veafSpawn.Id):debug("indexed newGroupName=%s",newGroupName) -- (re)spawn group local vars = {} vars.gpName = chosenTemplateName vars.name = chosenTemplateName --vars.groupData = _template:getGroupData() vars.route = newRoute vars.action = 'clone' vars.point = position vars.newGroupName = newGroupName local newGroup = mist.teleportToPoint(vars, true) if not newGroup then veaf.loggers.get(veafSpawn.Id):error("cannot respawn group %s",veaf.p(vars.name)) return nil end if country and #country > 0 then newGroup.countryId = veaf.getCountryId(country) end --newGroup.task = "CAP" --needs to be set in the editor veaf.loggers.get(veafSpawn.Id):trace("after preparation by MIST, newGroup=%s", veaf.p(newGroup, nil, {"route", "payload"})) newGroup.hidden = false newGroup.name = newGroupName newGroup.hiddenOnMFD = hiddenOnMFD for _, unit in pairs(newGroup.units) do unit.skill = skill local unitName = unit.unitName or unit.name veaf.loggers.get(veafSpawn.Id):trace("original unitName=%s",unitName) if not veafSpawn.spawnedNamesIndex[unitName] then veafSpawn.spawnedNamesIndex[unitName] = 1 else veafSpawn.spawnedNamesIndex[unitName] = veafSpawn.spawnedNamesIndex[unitName] + 1 end local spawnedUnitName = string.format("%s #%04d", unitName, veafSpawn.spawnedNamesIndex[unitName]) unit.name = spawnedUnitName unit.alt = position.y veaf.loggers.get(veafSpawn.Id):debug("indexed spawnedUnitName=%s",spawnedUnitName) end veaf.loggers.get(veafSpawn.Id):trace("before mist.dynAdd, newGroup=%s", veaf.p(newGroup, nil, {"route", "payload"})) local _spawnedGroup = mist.dynAdd(newGroup) if not _spawnedGroup then veaf.loggers.get(veafSpawn.Id):error("cannot spawn group %s",veaf.p(newGroup.name)) return nil end veaf.loggers.get(veafSpawn.Id):debug("after mist.dynAdd, _spawnedGroup.name=%s",_spawnedGroup.name) veaf.loggers.get(veafSpawn.Id):trace("after mist.dynAdd, _spawnedGroup=%s", veaf.p(_spawnedGroup, nil, {"route", "payload"})) local _dcsSpawnedGroup = Group.getByName(_spawnedGroup.name) veaf.loggers.get(veafSpawn.Id):trace("result of dcs side getByName, _dcsSpawnedGroup=%s", veaf.p(_dcsSpawnedGroup, nil, {"route", "payload"})) veaf.loggers.get(veafSpawn.Id):debug("result of dcs side getByName, _dcsSpawnedGroup.name=%s", _dcsSpawnedGroup:getName()) for index, unit in pairs(_dcsSpawnedGroup:getUnits()) do veaf.loggers.get(veafSpawn.Id):debug("result of dcs side getByName, _dcsSpawnedGroup.unit[%s].name=%s", index, unit:getName()) end local controller = _dcsSpawnedGroup:getController() controller:setOption(AI.Option.Air.id.PROHIBIT_AA, true) veaf.loggers.get(veafSpawn.Id):debug("starting CAP target watchdog...") mist.scheduleFunction(veafSpawn.startCapWatchdog, {_spawnedGroup.name, coalition, parameters.targetZone}, timer.getTime() + 1) local message = string.format("A CAP of %s (%s) has been spawned", name, country) veaf.loggers.get(veafSpawn.Id):info(message) if not silent then trigger.action.outText(message, 15) end return _spawnedGroup.name end function veafSpawn.startCapWatchdog(capGroupName, capCoalition, capZone, pTargetsList, pNumberOfTasksAddedByWatchdog) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.startCapWatchdog(capGroupName=%s)", veaf.p(capGroupName)) veaf.loggers.get(veafSpawn.Id):trace("capZone=%s", veaf.p(capZone)) if capGroupName == nil then veaf.loggers.get(veafSpawn.Id):error("veafSpawn.startCapWatchdog; capGroupName is mandatory !") return end if capCoalition == nil then veaf.loggers.get(veafSpawn.Id):error("veafSpawn.startCapWatchdog; capCoalition is mandatory !") return end local capGroup = Group.getByName(capGroupName) if not capGroup then veaf.loggers.get(veafSpawn.Id):debug("CAP group %s is nowhere to be found, stopping watchdog", veaf.p(capGroupName)) return end local capGroupPosition = veaf.getAveragePosition(capGroup) if not capGroupPosition then veaf.loggers.get(veafSpawn.Id):error("CAP group %s has no position!", veaf.p(capGroupName)) return end veaf.loggers.get(veafSpawn.Id):trace("Looking in CAP zone for targets...") local timestamp = timer.getTime() local targetsList = pTargetsList or {} local numberOfTasksAddedByWatchdog = pNumberOfTasksAddedByWatchdog or 0 veaf.loggers.get(veafSpawn.Id):trace("targetsList=%s", veaf.p(targetsList)) veaf.loggers.get(veafSpawn.Id):trace("numberOfTasksAddedByWatchdog=%s", veaf.p(numberOfTasksAddedByWatchdog)) -- check CAP group for state and position local capLanded = true local capInZone = false for _,unit in pairs(capGroup:getUnits()) do if unit and unit:inAir() then capLanded = false local isUnitInZone = veaf.isUnitInZone(unit, capZone) veaf.loggers.get(veafSpawn.Id):trace("unitName=%s, isUnitInZone=%s", veaf.p(unit:getName()), veaf.p(isUnitInZone)) if isUnitInZone then capInZone = true -- unit is in the zone, and in the air, let's test the targets it can see local detectedTargets = unit:getController():getDetectedTargets() if detectedTargets and #detectedTargets > 0 then -- process each target and compute its priority, then add it to the targets list for _, detectedTarget in pairs(detectedTargets) do local target = detectedTarget.object local targetId = target:getID() local targetGroup = target:getGroup() local targetGroupName = targetGroup:getName() local targetName = target:getName() veaf.loggers.get(veafSpawn.Id):trace("Checking targetGroupName=%s, targetName=%s, targetId=%s", veaf.p(targetGroupName), veaf.p(targetName), veaf.p(targetId)) local targetIsAirborne = target:isActive() and target:inAir() local targetCoalition = target:getCoalition() local targetGroupCategory = targetGroup:getCategory() veaf.loggers.get(veafSpawn.Id):trace("targetIsAirborne=%s", veaf.p(targetIsAirborne)) veaf.loggers.get(veafSpawn.Id):trace("targetCoalition=%s", veaf.p(targetCoalition)) veaf.loggers.get(veafSpawn.Id):trace("targetGroupCategory=%s", veaf.p(targetGroupCategory)) if targetIsAirborne ~= nil and targetGroupCategory ~= nil and targetCoalition ~= nil and targetCoalition ~= capCoalition and (targetGroupCategory == Group.Category.AIRPLANE or targetGroupCategory == Group.Category.HELICOPTER) then local targetPosition = target:getPosition().p local targetDistanceFromCapZoneCenter = mist.utils.get2DDist(targetPosition, capZone) veaf.loggers.get(veafSpawn.Id):trace("targetPosition=%s", veaf.p(targetPosition)) veaf.loggers.get(veafSpawn.Id):trace("targetDistanceFromCapZoneCenter=%s", veaf.p(targetDistanceFromCapZoneCenter)) if targetDistanceFromCapZoneCenter <= capZone.radius then -- consider only the targets that are in the CAP zone local targetAttributes = target:getDesc().attributes local targetType = target:getTypeName() local targetDistanceFromCapGroup = mist.utils.get2DDist(targetPosition, capGroupPosition) veaf.loggers.get(veafSpawn.Id):trace("targetType=%s", veaf.p(targetType)) veaf.loggers.get(veafSpawn.Id):trace("targetAttributes=%s", veaf.p(targetAttributes)) veaf.loggers.get(veafSpawn.Id):trace("targetDistanceFromCapGroup=%s", veaf.p(targetDistanceFromCapGroup)) local targetPriority = nil if targetAttributes["Fighters"] or targetAttributes["Multirole fighters"] then veaf.loggers.get(veafSpawn.Id):trace("Target is a Fighter") targetPriority = math.floor(targetDistanceFromCapGroup/2) elseif targetAttributes["Strategic bombers"] then veaf.loggers.get(veafSpawn.Id):trace("Target is a strategic bomber") targetPriority = math.floor(targetDistanceFromCapGroup/1.5) + 10000 elseif targetAttributes["Bombers"] then veaf.loggers.get(veafSpawn.Id):trace("Target is a bomber") targetPriority = math.floor(targetDistanceFromCapGroup/1) + 15000 elseif targetAttributes["UAVs"] and targetType ~= "Yak-52" then --wtf ED, Yak-52 UAV master race veaf.loggers.get(veafSpawn.Id):trace("Target is a UAV (except the Yak-52, that shit is not a UAV ED)") targetPriority = math.floor(targetDistanceFromCapGroup/0.5) + 15000 elseif targetAttributes["AWACS"] then veaf.loggers.get(veafSpawn.Id):trace("Target is an AWACS") targetPriority = math.floor(targetDistanceFromCapGroup/0.5) + 15000 elseif targetAttributes["Transports"] then veaf.loggers.get(veafSpawn.Id):trace("Target is a Transport") targetPriority = math.floor(targetDistanceFromCapGroup/0.5) + 15000 elseif targetAttributes["Battle airplanes"] or targetAttributes["Battleplanes"] then veaf.loggers.get(veafSpawn.Id):trace("Target is a generic Battleplane") targetPriority = math.floor(targetDistanceFromCapGroup/0.25) + 15000 elseif targetAttributes["Helicopters"] or targetAttributes["Attack helicopters"] or targetAttributes["Transport helicopters"] then veaf.loggers.get(veafSpawn.Id):trace("Target is a Helicopter") targetPriority = math.floor(targetDistanceFromCapGroup/0.1) + 20000 else veaf.loggers.get(veafSpawn.Id):trace("Target has unknown attributes, calculating generic priority") targetPriority = math.floor(targetDistanceFromCapGroup/0.25) + 15000 end -- https://www.geogebra.org/calculator if you want to visualize, type in functions y=x/factor + offset and set points on each curve. y is the priority, x the distance veaf.loggers.get(veafSpawn.Id):trace("priority=%s", veaf.p(targetPriority)) local targetData = targetsList[targetId] if targetData then -- this target has already been detected; was it in the same run, by another plane from the CAP group ? if targetData.seenAt == timestamp then -- yes, we can only increase the priority (never decrease it) veaf.loggers.get(veafSpawn.Id):debug("redetection (same run) of targetName=%s", veaf.p(targetName)) if targetData.priority < targetPriority then veaf.loggers.get(veafSpawn.Id):debug("increasing priority of targetName=%s to %s", veaf.p(targetName), veaf.p(targetPriority)) targetData.priority = targetPriority end else -- no, it's an old target, let's mark it as old veaf.loggers.get(veafSpawn.Id):debug("redetection (previous run) of targetName=%s", veaf.p(targetName)) targetData.isNew = false end else -- new target! register in into the target list veaf.loggers.get(veafSpawn.Id):debug("new detection of targetName=%s, priority=%s", veaf.p(targetName), veaf.p(targetPriority)) targetsList[targetId] = {isNew = true, seenAt = timestamp, priority = targetPriority, targetId = targetId, unit = unit} end end end end end end end end if capLanded then capGroup:destroy() veaf.loggers.get(veafSpawn.Id):debug("CAP group %s is landed, destroying it and stopping watchdog", veaf.p(capGroupName)) return end local controller = capGroup:getController() if capInZone then veaf.loggers.get(veafSpawn.Id):debug("CAP group is still in the CAP zone...") if not controller then veaf.loggers.get(veafSpawn.Id):error("cannot find controller for CAP group!") return end controller:setOption(AI.Option.Air.id.PROHIBIT_AA, false) controller:setOption(0,0) --weapons free --sort the list in reverse priority order so that the last task to be pushed in spot #1 is the one with the lowest priority, couldn't quite figure out which way works best, since this one makes the least sense it seems appropriate for DCS table.sort(targetsList, function(a,b) return a.priority < b.priority end) veaf.loggers.get(veafSpawn.Id):trace("targetsList=%s", veaf.p(targetsList)) local foundTargets = false for targetId, targetData in pairs(targetsList) do if not foundTargets then -- only write that once! veaf.loggers.get(veafSpawn.Id):debug("Watchdog has targets ! Allowing AA for CAP") foundTargets = true end if not Unit.isExist(targetData.unit) or not targetData.unit:inAir() or timestamp > targetData.seenAt + veafSpawn.CAP_WATCHDOG_DELAY*2 then veaf.loggers.get(veafSpawn.Id):trace("Target is outdated, landed or doesn't exist, removing it from the list") targetsList[targetId] = nil else veaf.loggers.get(veafSpawn.Id):trace("Engaging target!") local engageUnit = { id = 'EngageUnit', params = { unitId = targetId, weaponType = "ALL", priority = targetData.priority, } } controller:pushTask(engageUnit) numberOfTasksAddedByWatchdog = numberOfTasksAddedByWatchdog + 1 end end if not foundTargets then -- no targets, let's remove all the tasks we added (taking care not to remove the original task, which is to fly along the route) veaf.loggers.get(veafSpawn.Id):debug("Watchdog found no targets, removing all tasks and prohibiting AA for CAP") while controller:hasTask() and numberOfTasksAddedByWatchdog > 0 do veaf.loggers.get(veafSpawn.Id):trace("numberOfTasksAddedByWatchdog=%s", veaf.p(numberOfTasksAddedByWatchdog)) controller:resetTask() veaf.loggers.get(veafSpawn.Id):trace("resetTask() called") numberOfTasksAddedByWatchdog = numberOfTasksAddedByWatchdog - 1 end controller:setOption(AI.Option.Air.id.PROHIBIT_AA, true) controller:setOption(0,3) --return fire end else veaf.loggers.get(veafSpawn.Id):debug("CAP is outside of its area ! Discarding targets...") controller:setOption(AI.Option.Air.id.PROHIBIT_AA, true) controller:setOption(0,3) --return fire end veaf.loggers.get(veafSpawn.Id):debug(string.format("Rescheduling watchdog in %s seconds", veafSpawn.CAP_WATCHDOG_DELAY)) veaf.loggers.get(veafSpawn.Id):debug("===============================================================================") mist.scheduleFunction(veafSpawn.startCapWatchdog, {capGroupName, capCoalition, capZone, targetsList, numberOfTasksAddedByWatchdog}, timer.getTime() + veafSpawn.CAP_WATCHDOG_DELAY) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Mission master features ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafSpawn.missionMasterRunnables = {} veafSpawn.missionMasterRunnables.__silent = true function veafSpawn.missionMasterSetMessagingMode(silent, toGroupId) veafSpawn.missionMasterRunnables.__silent = silent veafSpawn.missionMasterRunnables.__toGroupId = toGroupId end function veafSpawn.missionMasterOutText(message) -- don't send the message if __silent is true if not(veafSpawn.missionMasterRunnables.__silent) then if (veafSpawn.missionMasterRunnables.__toGroupId) then -- send to a group trigger.action.outTextForGroup(veafSpawn.missionMasterRunnables.__toGroupId, message, 5) else -- send to all trigger.action.outText(message, 5) end end end function veafSpawn.missionMasterAddRunnable(name, code, parameters) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterAddRunnable(name=%s)",name) veafSpawn.missionMasterRunnables[veaf.ifnn(name, "upper")] = { code, parameters } end function veafSpawn.missionMasterRun(name) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterRun(name=%s)",name) if not name or #name == 0 then local message = "Mission Master, `run` requires the name of the code to be run" veaf.loggers.get(veafSpawn.Id):warn(message) veafSpawn.missionMasterOutText(message) return end local code, parameters = veaf.safeUnpack(veafSpawn.missionMasterRunnables[veaf.ifnn(name, "upper")]) if code then local sta, res = pcall(code, parameters) if sta then local message = string.format("Mission Master, the runnable [%s] was successfully run and returned : %s", name, veaf.p(res)) veaf.loggers.get(veafSpawn.Id):warn(message) veafSpawn.missionMasterOutText(message) else local message = string.format("Mission Master, the runnable [%s] returned an error : %s", name, veaf.p(res)) veaf.loggers.get(veafSpawn.Id):warn(message) veafSpawn.missionMasterOutText(message) end else local message = string.format("Mission Master, the runnable [%s] does not exist", name) veaf.loggers.get(veafSpawn.Id):warn(message) veafSpawn.missionMasterOutText(message) end end function veafSpawn.missionMasterSetFlagFromTable(parameters) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterSetFlagFromTable(parameters=%s)", parameters) local name, value = veaf.safeUnpack(parameters) return veafSpawn.missionMasterSetFlag(name, value) end function veafSpawn.missionMasterIncrementFlagValue(name) veafSpawn.missionMasterAddValueToFlag(name, 1) end function veafSpawn.missionMasterDecrementFlagValue(name) veafSpawn.missionMasterAddValueToFlag(name, -1) end function veafSpawn.missionMasterAddValueToFlag(name, increment) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterIncrementFlagValue(name=%s, increment=%s)", name, increment) if not name then local message = "Mission Master, `setFlag` requires the name or number of the flag" veaf.loggers.get(veafSpawn.Id):warn(message) veafSpawn.missionMasterOutText(message) return end local value = trigger.misc.getUserFlag(name) if not value then value = 0 end trigger.action.setUserFlag(name , value + increment) end function veafSpawn.missionMasterSetFlag(name, value) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterSetFlag(name=%s, value=%s)", name, value) if not name then local message = "Mission Master, `setFlag` requires the name or number of the flag" veaf.loggers.get(veafSpawn.Id):warn(message) veafSpawn.missionMasterOutText(message) return end trigger.action.setUserFlag(name , value) end function veafSpawn.missionMasterGetFlag(name) veaf.loggers.get(veafSpawn.Id):debug("veafSpawn.missionMasterGetFlag(name=%s)", name) if not name then local message = "Mission Master, `getFlag` requires the name or number of the flag" veaf.loggers.get(veafSpawn.Id):warn(message) veafSpawn.missionMasterOutText(message) return end local value = trigger.misc.getUserFlag(name) local message = string.format("Mission Master, flag [%s] has value [%s]", name, veaf.p(value)) veaf.loggers.get(veafSpawn.Id):info(message) veafSpawn.missionMasterOutText(message) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Build the initial radio menu function veafSpawn.buildRadioMenu() veaf.loggers.get(veafSpawn.Id):debug(string.format("veafSpawn.buildRadioMenu() hideMenu%s", veaf.p(veafSpawn.HideRadioMenu))) if not veafSpawn.HideRadioMenu then veafSpawn.rootPath = veafRadio.addSubMenu(veafSpawn.RadioMenuName) veafRadio.addCommandToSubmenu("Available Aircraft spawns", veafSpawn.rootPath, veafSpawn.listAllCAP, nil, veafRadio.USAGE_ForAll) veafRadio.addCommandToSubmenu("Info on all convoys", veafSpawn.rootPath, veafSpawn.infoOnAllConvoys, nil, veafRadio.USAGE_ForGroup) local menuPath = veafRadio.addSubMenu("Mark closest convoy route", veafSpawn.rootPath) veafRadio.addCommandToSubmenu("Mark closest convoy route" , menuPath, veafSpawn.markClosestConvoyRouteWithSmoke, nil, veafRadio.USAGE_ForGroup) local menuPath = veafRadio.addSubMenu("Mark closest convoy", veafSpawn.rootPath) veafRadio.addCommandToSubmenu("Mark closest convoy" , menuPath, veafSpawn.markClosestConvoyWithSmoke, nil, veafRadio.USAGE_ForGroup) local menuPath = veafRadio.addSubMenu("Stop closest convoy", veafSpawn.rootPath) veafRadio.addCommandToSubmenu("Stop closest convoy" , menuPath, veafSpawn.stopClosestConvoy, nil, veafRadio.USAGE_ForGroup) local menuPath = veafRadio.addSubMenu("Makes closest convoy move", veafSpawn.rootPath) veafRadio.addCommandToSubmenu("Make closest convoy move" , menuPath, veafSpawn.moveClosestConvoy, nil, veafRadio.USAGE_ForGroup) veafRadio.addSecuredCommandToSubmenu('Cleanup all convoys', veafSpawn.rootPath, veafSpawn.cleanupAllConvoys) veafRadio.refreshRadioMenu() end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafSpawn.initialize() veafSpawn.buildRadioMenu() veafSpawn.initializeAirUnitTemplates() veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafSpawn.onEventMarkChange) veafSpawn.dumpSpawnablePlanesList() end veaf.loggers.get(veafSpawn.Id):info(string.format("Loading version %s", veafSpawn.Version)) ------------------ END script veafSpawn.lua ------------------ ------------------ START script veafSecurity.lua ------------------ ------------------------------------------------------------------ -- VEAF security function library for DCS World -- By zip (2019) -- -- Features: -- --------- -- * Checks if the user is part of an authorized users shortlist -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafSecurity = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafSecurity.Id = "SECURITY" --- Version. veafSecurity.Version = "1.3.3" -- trace level, specific to this module --veafSecurity.LogLevel = "trace" veaf.loggers.new(veafSecurity.Id, veafSecurity.LogLevel) --- Key phrase to look for in the mark text which triggers the command. veafSecurity.Keyphrase = "_auth" veafSecurity.authDuration = 10 veafSecurity.RemoteCommandParser = "([[a-zA-Z0-9]+)%s?(.*)" veafSecurity.LEVEL_L0 = 90 veafSecurity.LEVEL_L1 = 10 veafSecurity.LEVEL_L9 = 1 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafSecurity.password_L0 = {} veafSecurity.password_L1 = {} veafSecurity.password_L9 = {} veafSecurity.password_MM = {} -- list the security passwords common to all missions below veafSecurity.PASSWORD_L0 = "47c7808d1079fd20add322bbd5cf23b93ad1841e" veafSecurity.PASSWORD_L1 = "bdc82f5ef92369919a3a53515023ce19f68656cc" veafSecurity.password_L0[veafSecurity.PASSWORD_L0] = true veafSecurity.password_L1[veafSecurity.PASSWORD_L1] = true veafSecurity.authenticated = veaf.SecurityDisabled ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- SHA-1 pure LUA implementation ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- $Revision: 1.5 $ -- $Date: 2014-09-10 16:54:25 $ -- This module was originally taken from http://cube3d.de/uploads/Main/sha1.txt. ------------------------------------------------------------------------------- -- SHA-1 secure hash computation, and HMAC-SHA1 signature computation, -- in pure Lua (tested on Lua 5.1) -- License: MIT -- -- Usage: -- local hashAsHex = sha1.hex(message) -- returns a hex string -- local hashAsData = sha1.bin(message) -- returns raw bytes -- -- local hmacAsHex = sha1.hmacHex(key, message) -- hex string -- local hmacAsData = sha1.hmacBin(key, message) -- raw bytes -- -- -- Pass sha1.hex() a string, and it returns a hash as a 40-character hex string. -- For example, the call -- -- local hash = sha1.hex("iNTERFACEWARE") -- -- puts the 40-character string -- -- "e76705ffb88a291a0d2f9710a5471936791b4819" -- -- into the variable 'hash' -- -- Pass sha1.hmacHex() a key and a message, and it returns the signature as a -- 40-byte hex string. -- -- -- The two "bin" versions do the same, but return the 20-byte string of raw -- data that the 40-byte hex strings represent. -- ------------------------------------------------------------------------------- -- -- Description -- Due to the lack of bitwise operations in 5.1, this version uses numbers to -- represents the 32bit words that we combine with binary operations. The basic -- operations of byte based "xor", "or", "and" are all cached in a combination -- table (several 64k large tables are built on startup, which -- consumes some memory and time). The caching can be switched off through -- setting the local cfg_caching variable to false. -- For all binary operations, the 32 bit numbers are split into 8 bit values -- that are combined and then merged again. -- -- Algorithm: http://www.itl.nist.gov/fipspubs/fip180-1.htm -- ------------------------------------------------------------------------------- sha1 = {} -- set this to false if you don't want to build several 64k sized tables when -- loading this file (takes a while but grants a boost of factor 13) local cfg_caching = false -- local storing of global functions (minor speedup) local floor,modf = math.floor,math.modf local char,format,rep = string.char,string.format,string.rep -- merge 4 bytes to an 32 bit word local function bytes_to_w32 (a,b,c,d) return a*0x1000000+b*0x10000+c*0x100+d end -- split a 32 bit word into four 8 bit numbers local function w32_to_bytes (i) return floor(i/0x1000000)%0x100,floor(i/0x10000)%0x100,floor(i/0x100)%0x100,i%0x100 end -- shift the bits of a 32 bit word. Don't use negative values for "bits" local function w32_rot (bits,a) local b2 = 2^(32-bits) local a,b = modf(a/b2) return a+b*b2*(2^(bits)) end -- caching function for functions that accept 2 arguments, both of values between -- 0 and 255. The function to be cached is passed, all values are calculated -- during loading and a function is returned that returns the cached values (only) local function cache2arg (fn) if not cfg_caching then return fn end local lut = {} for i=0,0xffff do local a,b = floor(i/0x100),i%0x100 lut[i] = fn(a,b) end return function (a,b) return lut[a*0x100+b] end end -- splits an 8-bit number into 8 bits, returning all 8 bits as booleans local function byte_to_bits (b) local b = function (n) local b = floor(b/n) return b%2==1 end return b(1),b(2),b(4),b(8),b(16),b(32),b(64),b(128) end -- builds an 8bit number from 8 booleans local function bits_to_byte (a,b,c,d,e,f,g,h) local function n(b,x) return b and x or 0 end return n(a,1)+n(b,2)+n(c,4)+n(d,8)+n(e,16)+n(f,32)+n(g,64)+n(h,128) end -- debug function for visualizing bits in a string local function bits_to_string (a,b,c,d,e,f,g,h) local function x(b) return b and "1" or "0" end return ("%s%s%s%s %s%s%s%s"):format(x(a),x(b),x(c),x(d),x(e),x(f),x(g),x(h)) end -- debug function for converting a 8-bit number as bit string local function byte_to_bit_string (b) return bits_to_string(byte_to_bits(b)) end -- debug function for converting a 32 bit number as bit string local function w32_to_bit_string(a) if type(a) == "string" then return a end local aa,ab,ac,ad = w32_to_bytes(a) local s = byte_to_bit_string return ("%s %s %s %s"):format(s(aa):reverse(),s(ab):reverse(),s(ac):reverse(),s(ad):reverse()):reverse() end -- bitwise "and" function for 2 8bit number local band = cache2arg (function(a,b) local A,B,C,D,E,F,G,H = byte_to_bits(b) local a,b,c,d,e,f,g,h = byte_to_bits(a) return bits_to_byte( A and a, B and b, C and c, D and d, E and e, F and f, G and g, H and h) end) -- bitwise "or" function for 2 8bit numbers local bor = cache2arg(function(a,b) local A,B,C,D,E,F,G,H = byte_to_bits(b) local a,b,c,d,e,f,g,h = byte_to_bits(a) return bits_to_byte( A or a, B or b, C or c, D or d, E or e, F or f, G or g, H or h) end) -- bitwise "xor" function for 2 8bit numbers local bxor = cache2arg(function(a,b) local A,B,C,D,E,F,G,H = byte_to_bits(b) local a,b,c,d,e,f,g,h = byte_to_bits(a) return bits_to_byte( A ~= a, B ~= b, C ~= c, D ~= d, E ~= e, F ~= f, G ~= g, H ~= h) end) -- bitwise complement for one 8bit number local function bnot (x) return 255-(x % 256) end -- creates a function to combine to 32bit numbers using an 8bit combination function local function w32_comb(fn) return function (a,b) local aa,ab,ac,ad = w32_to_bytes(a) local ba,bb,bc,bd = w32_to_bytes(b) return bytes_to_w32(fn(aa,ba),fn(ab,bb),fn(ac,bc),fn(ad,bd)) end end -- create functions for and, xor and or, all for 2 32bit numbers local w32_and = w32_comb(band) local w32_xor = w32_comb(bxor) local w32_or = w32_comb(bor) -- xor function that may receive a variable number of arguments local function w32_xor_n (a,...) local aa,ab,ac,ad = w32_to_bytes(a) for i=1,select('#',...) do local ba,bb,bc,bd = w32_to_bytes(select(i,...)) aa,ab,ac,ad = bxor(aa,ba),bxor(ab,bb),bxor(ac,bc),bxor(ad,bd) end return bytes_to_w32(aa,ab,ac,ad) end -- combining 3 32bit numbers through binary "or" operation local function w32_or3 (a,b,c) local aa,ab,ac,ad = w32_to_bytes(a) local ba,bb,bc,bd = w32_to_bytes(b) local ca,cb,cc,cd = w32_to_bytes(c) return bytes_to_w32( bor(aa,bor(ba,ca)), bor(ab,bor(bb,cb)), bor(ac,bor(bc,cc)), bor(ad,bor(bd,cd)) ) end -- binary complement for 32bit numbers local function w32_not (a) return 4294967295-(a % 4294967296) end -- adding 2 32bit numbers, cutting off the remainder on 33th bit local function w32_add (a,b) return (a+b) % 4294967296 end -- adding n 32bit numbers, cutting off the remainder (again) local function w32_add_n (a,...) for i=1,select('#',...) do a = (a+select(i,...)) % 4294967296 end return a end -- converting the number to a hexadecimal string local function w32_to_hexstring (w) return format("%08x",w) end -- calculating the SHA1 for some text function sha1.hex(msg) local H0,H1,H2,H3,H4 = 0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476,0xC3D2E1F0 local msg_len_in_bits = #msg * 8 local first_append = char(0x80) -- append a '1' bit plus seven '0' bits local non_zero_message_bytes = #msg +1 +8 -- the +1 is the appended bit 1, the +8 are for the final appended length local current_mod = non_zero_message_bytes % 64 local second_append = current_mod>0 and rep(char(0), 64 - current_mod) or "" -- now to append the length as a 64-bit number. local B1, R1 = modf(msg_len_in_bits / 0x01000000) local B2, R2 = modf( 0x01000000 * R1 / 0x00010000) local B3, R3 = modf( 0x00010000 * R2 / 0x00000100) local B4 = 0x00000100 * R3 local L64 = char( 0) .. char( 0) .. char( 0) .. char( 0) -- high 32 bits .. char(B1) .. char(B2) .. char(B3) .. char(B4) -- low 32 bits msg = msg .. first_append .. second_append .. L64 assert(#msg % 64 == 0) local chunks = #msg / 64 local W = { } local start, A, B, C, D, E, f, K, TEMP local chunk = 0 while chunk < chunks do -- -- break chunk up into W[0] through W[15] -- start,chunk = chunk * 64 + 1,chunk + 1 for t = 0, 15 do W[t] = bytes_to_w32(msg:byte(start, start + 3)) start = start + 4 end -- -- build W[16] through W[79] -- for t = 16, 79 do -- For t = 16 to 79 let Wt = S1(Wt-3 XOR Wt-8 XOR Wt-14 XOR Wt-16). W[t] = w32_rot(1, w32_xor_n(W[t-3], W[t-8], W[t-14], W[t-16])) end A,B,C,D,E = H0,H1,H2,H3,H4 for t = 0, 79 do if t <= 19 then -- (B AND C) OR ((NOT B) AND D) f = w32_or(w32_and(B, C), w32_and(w32_not(B), D)) K = 0x5A827999 elseif t <= 39 then -- B XOR C XOR D f = w32_xor_n(B, C, D) K = 0x6ED9EBA1 elseif t <= 59 then -- (B AND C) OR (B AND D) OR (C AND D f = w32_or3(w32_and(B, C), w32_and(B, D), w32_and(C, D)) K = 0x8F1BBCDC else -- B XOR C XOR D f = w32_xor_n(B, C, D) K = 0xCA62C1D6 end -- TEMP = S5(A) + ft(B,C,D) + E + Wt + Kt; A,B,C,D,E = w32_add_n(w32_rot(5, A), f, E, W[t], K), A, w32_rot(30, B), C, D end -- Let H0 = H0 + A, H1 = H1 + B, H2 = H2 + C, H3 = H3 + D, H4 = H4 + E. H0,H1,H2,H3,H4 = w32_add(H0, A),w32_add(H1, B),w32_add(H2, C),w32_add(H3, D),w32_add(H4, E) end local f = w32_to_hexstring return f(H0) .. f(H1) .. f(H2) .. f(H3) .. f(H4) end local function hex_to_binary(hex) return hex:gsub('..', function(hexval) return string.char(tonumber(hexval, 16)) end) end function sha1.bin(msg) return hex_to_binary(sha1.hex(msg)) end local xor_with_0x5c = {} local xor_with_0x36 = {} -- building the lookuptables ahead of time (instead of littering the source code -- with precalculated values) for i=0,0xff do xor_with_0x5c[char(i)] = char(bxor(i,0x5c)) xor_with_0x36[char(i)] = char(bxor(i,0x36)) end local blocksize = 64 -- 512 bits function sha1.hmacHex(key, text) assert(type(key) == 'string', "key passed to hmacHex should be a string") assert(type(text) == 'string', "text passed to hmacHex should be a string") if #key > blocksize then key = sha1.bin(key) end local key_xord_with_0x36 = key:gsub('.', xor_with_0x36) .. string.rep(string.char(0x36), blocksize - #key) local key_xord_with_0x5c = key:gsub('.', xor_with_0x5c) .. string.rep(string.char(0x5c), blocksize - #key) return sha1.hex(key_xord_with_0x5c .. sha1.bin(key_xord_with_0x36 .. text)) end function sha1.hmacBin(key, text) return hex_to_binary(sha1.hmacHex(key, text)) end ---------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- execute command from the remote interface function veafSecurity.executeCommandFromRemote(parameters) veaf.loggers.get(veafSecurity.Id):debug(string.format("veafSecurity.executeCommandFromRemote()")) veaf.loggers.get(veafSecurity.Id):trace(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) veaf.loggers.get(veafSecurity.Id):trace(string.format("_pilot= %s", veaf.p(_pilot))) veaf.loggers.get(veafSecurity.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName))) veaf.loggers.get(veafSecurity.Id):trace(string.format("_unitName= %s", veaf.p(_unitName))) veaf.loggers.get(veafSecurity.Id):trace(string.format("_command= %s", veaf.p(_command))) if not _pilot or not _command then return false end if _command then -- parse the command local _action, _parameters = _command:match(veafSecurity.RemoteCommandParser) veaf.loggers.get(veafSecurity.Id):trace(string.format("_action=%s",veaf.p(_action))) veaf.loggers.get(veafSecurity.Id):trace(string.format("_parameters=%s",veaf.p(_parameters))) if _action and _action:lower() == "login" then if _pilot.level >= veafSecurity.LEVEL_L1 then veaf.loggers.get(veafSecurity.Id):info(string.format("[%s] is unlocking the mission",veaf.p(_pilotName))) veafSecurity.authenticate(_parameters, _unitName) return true else veaf.loggers.get(veafSecurity.Id):warn(string.format("[%s] has not the required level to unlock the mission",veaf.p(_pilotName))) return false end elseif _action and _action:lower() == "logout" then if _pilot.level >= veafSecurity.LEVEL_L1 then local _silent = _parameters and _parameters:lower() == "silent" veaf.loggers.get(veafSecurity.Id):info(string.format("[%s] is locking the mission",veaf.p(_pilotName))) veafSecurity.logout(not _silent, _unitName) return true else veaf.loggers.get(veafSecurity.Id):warn(string.format("[%s] has not the required level to lock the mission",veaf.p(_pilotName))) return false end end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function executed when a mark has changed. This happens when text is entered or changed. function veafSecurity.onEventMarkChange(eventPos, event) if veafSecurity.executeCommand(eventPos, event.text) then -- Delete old mark. veaf.loggers.get(veafSecurity.Id):trace(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafSecurity.executeCommand(eventPos, eventText, bypassSecurity) -- Check if marker has a text and the veafCasMission.keyphrase keyphrase. if eventText ~= nil and eventText:lower():find(veafSecurity.Keyphrase) then -- Analyse the mark point text and extract the keywords. local options = veafSecurity.markTextAnalysis(eventText) if options then -- Check options commands if options.login then -- check password if not (bypassSecurity or veafSecurity.checkPassword_L1(options.password)) then trigger.action.outText("password was not set or was not correct", 5) return false end veafSecurity.authenticate() return true elseif options.logout then veafSecurity.logout(true) return true end end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Analyse the mark text and extract keywords. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Extract keywords from mark text. function veafSecurity.markTextAnalysis(text) -- Option parameters extracted from the mark text. local switch = {} switch.login = false switch.logout = false -- password switch.password = nil -- Check for correct keywords. local pos = text:lower():find(veafSecurity.Keyphrase) if not pos then return nil end -- the logout command or the password should follow a space local text = text:sub(pos+string.len(veafSecurity.Keyphrase)+1) if text and text:lower() == "logout" then switch.logout = true else switch.password = text switch.login = true ----veaf.loggers.get(veafSecurity.Id):trace(string.format("switch.password=[%s]",switch.password)) end return switch end function veafSecurity.logout(withMessage, unitName) if not veafSecurity.authenticated and withMessage then veaf.outTextForUnit(unitName, "The system was already locked down", 5) return end veafSecurity.authenticated = false if withMessage then veaf.outTextForUnit(unitName, "The system has been locked down", 5) end veafRadio.refreshRadioMenu() if veafSecurity.logoutWatchdog then mist.removeFunction(veafSecurity.logoutWatchdog) end end --- authenticate all radios for a short time function veafSecurity.authenticate(minutes, unitName) local actualMinutes = minutes or veafSecurity.authDuration if type(actualMinutes) == "string" and not(actualMinutes:match("%d+")) then actualMinutes = veafSecurity.authDuration end if not veafSecurity.authenticated then veaf.outTextForUnit(unitName, string.format("The system is authenticated for %d minutes", actualMinutes), 15) veafSecurity.authenticated = true veafRadio.refreshRadioMenu() if veafSecurity.logoutWatchdog then mist.removeFunction(veafSecurity.logoutWatchdog) end veafSecurity.logoutWatchdog = mist.scheduleFunction(veafSecurity.logout,{true},timer.getTime()+actualMinutes*60) end end function veafSecurity._checkPassword(password, level) if password == nil then return false end veaf.loggers.get(veafSecurity.Id):debug(string.format("checkPassword(password = %s)",password)) local hash = sha1.hex(password) veaf.loggers.get(veafSecurity.Id):trace(string.format("hash = [%s]",hash)) if level[hash] ~= nil then veaf.loggers.get(veafSecurity.Id):debug("user authenticated") return true else veaf.loggers.get(veafSecurity.Id):debug("user not found") return false end end function veafSecurity.checkPassword_L0(password) return veaf.SecurityDisabled or veafSecurity._checkPassword(password, veafSecurity.password_L0) end function veafSecurity.checkPassword_L1(password) return veaf.SecurityDisabled or veafSecurity._checkPassword(password, veafSecurity.password_L1) or veafSecurity._checkPassword(password, veafSecurity.password_L0) end function veafSecurity.checkPassword_L9(password) return veaf.SecurityDisabled or veafSecurity._checkPassword(password, veafSecurity.password_L9) or veafSecurity._checkPassword(password, veafSecurity.password_L1) or veafSecurity._checkPassword(password, veafSecurity.password_L0) end function veafSecurity.checkPassword_MM(password) return veaf.SecurityDisabled or veafSecurity._checkPassword(password, veafSecurity.password_MM) end function veafSecurity.getMarkerSecurityLevel(markId) veaf.loggers.get(veafSecurity.Id):trace(string.format("veafSecurity.getMarkerSecurityLevel([%s])",veaf.p(markId))) local _author = nil for _, panel in pairs(world.getMarkPanels( )) do veaf.loggers.get(veafSecurity.Id):trace("panel=%s", veaf.p(panel)) if panel.idx == markId then _author = panel.author end end if _author == nil then -- markId may actually be the username if called from veafRemote - yes I know it's ugly _author = markId end veaf.loggers.get(veafSecurity.Id):trace("_author=%s",_author) local _user = veafRemote.getRemoteUser(_author) veaf.loggers.get(veafSecurity.Id):trace(string.format("_user = [%s]",veaf.p(_user))) if _user then return _user.level end return -1 end function veafSecurity.checkSecurity_L0(password, markId) -- don't check the password if already logged in if veafSecurity.isAuthenticated() then return true end if veafSecurity.getMarkerSecurityLevel(markId) < veafSecurity.LEVEL_L0 and not veafSecurity.checkPassword_L0(password) then veaf.loggers.get(veafSecurity.Id):warn("You have to give the correct L0 password to do this") trigger.action.outText("Please use the ', password ' option", 5) return false end return true end function veafSecurity.checkSecurity_L1(password, markId) -- don't check the password if already logged in if veafSecurity.isAuthenticated() then return true end if veafSecurity.getMarkerSecurityLevel(markId) < veafSecurity.LEVEL_L1 and not veafSecurity.checkPassword_L1(password) then veaf.loggers.get(veafSecurity.Id):warn("You have to give the correct L1 password to do this") trigger.action.outText("Please use the ', password ' option", 5) return false end return true end function veafSecurity.checkSecurity_L9(password, markId) -- don't check the password if already logged in if veafSecurity.isAuthenticated() then return true end if veafSecurity.getMarkerSecurityLevel(markId) < veafSecurity.LEVEL_L9 and not veafSecurity.checkPassword_L9(password) then veaf.loggers.get(veafSecurity.Id):warn("You have to give the correct L9 password to do this") trigger.action.outText("Please use the ', password ' option", 5) return false end return true end function veafSecurity.checkSecurity_MM(password) if not veafSecurity.checkPassword_MM(password) then veaf.loggers.get(veafSecurity.Id):warn("You have to give the correct Mission Master password to do this") trigger.action.outText("Please use the ', password ' option", 5) return false end return true end function veafSecurity.isAuthenticated() return veafSecurity.authenticated or veafSecurity.SecurityDisabled end function veafSecurity.initialize() veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafSecurity.onEventMarkChange) veafSecurity.authenticated = veaf.SecurityDisabled end veaf.loggers.get(veafSecurity.Id):info(string.format("Loading version %s", veafSecurity.Version)) ------------------ END script veafSecurity.lua ------------------ ------------------ START script veafShortcuts.lua ------------------ ------------------------------------------------------------------ -- VEAF shortcuts supporting functions for DCS World -- By zip (2020) -- -- Features: -- --------- -- * This module offers support for commands aliases and radio menu shortcuts -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafShortcuts = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafShortcuts.Id = "SHORTCUTS" --- Version. veafShortcuts.Version = "1.41.0" -- trace level, specific to this module --veafShortcuts.LogLevel = "trace" veaf.loggers.new(veafShortcuts.Id, veafShortcuts.LogLevel) veafShortcuts.RadioMenuName = "SHORTCUTS" veafShortcuts.AliasStarter = "-" veafShortcuts.RemoteCommandParser = "([a-zA-Z0-9:\\.-]+)%s(.*)" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Aliases list (table of VeafAlias objects) veafShortcuts.aliases = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafAlias object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafAlias = {} function VeafAlias:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object -- name objectToCreate.name = nil -- description objectToCreate.description = nil -- hidden from HELP objectToCreate.hidden = false -- the command that must be substituted to the alias objectToCreate.veafCommand = nil -- list of parameters that will be randomized if not present objectToCreate.randomParameters = {} -- if TRUE, security is bypassed objectToCreate.bypassSecurity = false -- if set, the alias will actually be a batch of aliases to execute in order objectToCreate.batchAliases = nil -- if set, the alias is password protected with a specific password objectToCreate.password = nil -- if set, we'll consider that the alias ends with a comma (to easily add the first parameter) objectToCreate.endsWithComma = true return objectToCreate end --- --- setters and getters --- function VeafAlias:setName(value) veaf.loggers.get(veafShortcuts.Id):trace(string.format("VeafAlias[%s]:setName([%s])", veaf.p(self.name) or "", value or "")) self.name = value return self end function VeafAlias:getName() return self.name end function VeafAlias:setVeafCommand(value) veaf.loggers.get(veafShortcuts.Id):trace(string.format("VeafAlias[%s]:setVeafCommand([%s])", veaf.p(self.name), value or "")) self.veafCommand = value return self end function VeafAlias:getVeafCommand() return self.veafCommand end function VeafAlias:setEndsWithComma(value) veaf.loggers.get(veafShortcuts.Id):trace(string.format("VeafAlias[%s]:setEndsWithComma([%s])", veaf.p(self.name), value or "")) self.endsWithComma = value return self end function VeafAlias:isEndsWithComma() return self.endsWithComma end function VeafAlias:addRandomParameter(name, low, high) veaf.loggers.get(veafShortcuts.Id):trace(string.format("VeafAlias[%s]:addRandomParameter([%s], %s, %s)", veaf.p(self.name), name or "", low or "", high or "")) table.insert(self.randomParameters, { name = name, low = low or 1, high = high or 6}) return self end function VeafAlias:getRandomParameters() return self.randomParameters end function VeafAlias:dontEndWithComma() veaf.loggers.get(veafShortcuts.Id):trace(string.format("VeafAlias[%s]:dontEndWithComma()", veaf.p(self.name))) self:setEndsWithComma(false) return self end function VeafAlias:setDescription(value) veaf.loggers.get(veafShortcuts.Id):trace(string.format("VeafAlias[%s]:setDescription([%s])", veaf.p(self.name), value or "")) self.description = value return self end function VeafAlias:getDescription() return self.description end function VeafAlias:setBypassSecurity(value) veaf.loggers.get(veafShortcuts.Id):trace(string.format("VeafAlias[%s]:setBypassSecurity([%s])", veaf.p(self.name), tostring(value) or "")) self.bypassSecurity = value return self end function VeafAlias:isBypassSecurity() return self.bypassSecurity end function VeafAlias:setHidden(value) veaf.loggers.get(veafShortcuts.Id):trace(string.format("VeafAlias[%s]:setHidden([%s])", veaf.p(self.name), tostring(value) or "")) self.hidden = value return self end function VeafAlias:isHidden() return self.hidden end function VeafAlias:setBatchAliases(value) veaf.loggers.get(veafShortcuts.Id):trace("VeafAlias[%s]:setBatchAliases([%s])", veaf.p(self.name), veaf.p(value)) self.batchAliases = value -- by default, batches are hidden and have a L1 password self:setPassword(veafSecurity.PASSWORD_L1) self:setHidden(true) return self end function VeafAlias:getBatchAliases() return self.batchAliases end function VeafAlias:setPassword(value) veaf.loggers.get(veafShortcuts.Id):trace("VeafAlias[%s]:setPassword([%s])", veaf.p(self.name), veaf.p(value)) self.password = {} self.password[value] = true return self end function VeafAlias:hasPassword(value) return self.password and self.password[value] end function VeafAlias:execute(remainingCommand, position, coalition, markId, bypassSecurity, spawnedGroups, route) local function logDebug(message) veaf.loggers.get(veafShortcuts.Id):debug(message) return true end veaf.loggers.get(veafShortcuts.Id):trace(string.format("markId=[%s]",veaf.p(markId))) local command = self:getVeafCommand() for _, parameter in pairs(self:getRandomParameters()) do veaf.loggers.get(veafShortcuts.Id):trace(string.format("randomizing [%s]",parameter.name or "")) local value = math.random(parameter.low, parameter.high) veaf.loggers.get(veafShortcuts.Id):trace(string.format("got [%d]",value)) command = string.format("%s, %s %d",command, parameter.name, value) end if self:isEndsWithComma() then veaf.loggers.get(veafShortcuts.Id):trace("adding a comma") command = command .. ", " end local _bypassSecurity = bypassSecurity or self:isBypassSecurity() local command = command .. (remainingCommand or "") veaf.loggers.get(veafShortcuts.Id):trace(string.format("command = [%s]",command or "")) if logDebug("checking in veafShortcuts") and veafShortcuts.executeCommand(position, command, coalition, markId, _bypassSecurity, spawnedGroups, route) then return true elseif logDebug("checking in veafSpawn") and veafSpawn.executeCommand(position, command, coalition, markId, _bypassSecurity, spawnedGroups, nil, nil, route) then return true elseif logDebug("checking in veafNamedPoints") and veafNamedPoints.executeCommand(position, {text=command, coalition=-1}, _bypassSecurity) then return true elseif logDebug("checking in veafCasMission") and veafCasMission.executeCommand(position, command, coalition, _bypassSecurity) then return true elseif logDebug("checking in veafSecurity") and veafSecurity.executeCommand(position, command, _bypassSecurity) then return true elseif logDebug("checking in veafMove") and veafMove.executeCommand(position, command, _bypassSecurity) then return true elseif logDebug("checking in veafRadio") and veafRadio.executeCommand(position, command, coalition, _bypassSecurity) then return true elseif logDebug("checking in veafGroundAI") and veafGroundAI.executeCommand(position, command, coalition, _bypassSecurity) then return true elseif logDebug("checking in veafRemote") and veafRemote.executeCommand(position, command) then return true else return false end end --- --- other methods --- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafAliasForCombatMission object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafAliasForCombatMission = {} VeafAliasForCombatMission.__index = VeafAliasForCombatMission function VeafAliasForCombatMission:new() local self = setmetatable(mist.utils.deepCopy(VeafAlias:new()), VeafAliasForCombatMission) self:setPassword(veafSecurity.PASSWORD_L1) self:setHidden(true) return self end setmetatable(VeafAliasForCombatMission, {__index = VeafAlias}) --- --- overloaded members --- function VeafAliasForCombatMission:execute(remainingCommand, position, coalition, markId, bypassSecurity, spawnedGroups) veaf.loggers.get(veafShortcuts.Id):trace("VeafAliasForCombatMission[%s]:execute([%s])", veaf.p(self.name), veaf.p(remainingCommand)) local command = self:getVeafCommand() for _, parameter in pairs(self:getRandomParameters()) do veaf.loggers.get(veafShortcuts.Id):trace(string.format("randomizing [%s]",parameter.name or "")) local value = math.random(parameter.low, parameter.high) veaf.loggers.get(veafShortcuts.Id):trace(string.format("got [%d]",value)) command = string.format("%s, %s %d",command, parameter.name, value) end if self:isEndsWithComma() then veaf.loggers.get(veafShortcuts.Id):trace("adding a comma") command = command .. ", " end local _bypassSecurity = bypassSecurity or self:isBypassSecurity() local command = command .. (remainingCommand or "") veaf.loggers.get(veafShortcuts.Id):trace("command=%s", veaf.p(command)) local keywords = veaf.split(command, ",") local silent = false local missionName = nil local password = nil for _, keyphrase in pairs(keywords) do local str = veaf.breakString(veaf.trim(keyphrase), " ") local key = str[1] local val = str[2] or "" if key:lower() == "silent" then silent = true end if key:lower() == "name" then missionName = val end if key:lower() == "password" then password = val end end if not (bypassSecurity or veafSecurity.isAuthenticated()) then veaf.loggers.get(veafShortcuts.Id):trace("password=%s", veaf.p(password)) local hash = nil if password then hash = sha1.hex(password) end if not(self:hasPassword(hash)) then veaf.loggers.get(veafShortcuts.Id):warn("You have to give the correct alias password for %s to do this", self:getName()) trigger.action.outText("Please use the ', password ' option", 5) return false end end veaf.loggers.get(veafShortcuts.Id):trace("missionName=%s", veaf.p(missionName)) veaf.loggers.get(veafShortcuts.Id):trace("silent=%s", veaf.p(silent)) if not missionName or #missionName == 0 then local msg = string.format("VeafAliasForCombatMission: mission name is mandatory") veaf.loggers.get(veafShortcuts.Id):warn(msg) trigger.action.outText(msg, 5) return false end local mission = veafCombatMission.GetMission(missionName) if not mission then local msg = string.format("VeafAliasForCombatMission: mission %s does not exist", veaf.p(missionName)) veaf.loggers.get(veafShortcuts.Id):warn(msg) trigger.action.outText(msg, 5) return false end --veaf.loggers.get(veafShortcuts.Id):trace("mission=%s", veaf.p(mission)) if command:lower():sub(1, 5) == "start" then local result = veafCombatMission.ActivateMission(missionName, silent) return result elseif command:lower():sub(1, 4) == "stop" then local result = veafCombatMission.DesactivateMission(missionName, silent) return result end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafAliasForCombatZone object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafAliasForCombatZone = VeafAlias:new() function VeafAliasForCombatZone:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object self:setPassword(veafSecurity.PASSWORD_L1) self:setHidden(true) return objectToCreate end --- --- overloaded members --- function VeafAliasForCombatZone:execute(remainingCommand, position, coalition, markId, bypassSecurity, spawnedGroups) veaf.loggers.get(veafShortcuts.Id):trace("VeafAliasForCombatZone[%s]:execute([%s])", veaf.p(self.name), veaf.p(remainingCommand)) local command = self:getVeafCommand() for _, parameter in pairs(self:getRandomParameters()) do veaf.loggers.get(veafShortcuts.Id):trace(string.format("randomizing [%s]",parameter.name or "")) local value = math.random(parameter.low, parameter.high) veaf.loggers.get(veafShortcuts.Id):trace(string.format("got [%d]",value)) command = string.format("%s, %s %d",command, parameter.name, value) end if self:isEndsWithComma() then veaf.loggers.get(veafShortcuts.Id):trace("adding a comma") command = command .. ", " end local _bypassSecurity = bypassSecurity or self:isBypassSecurity() local command = command .. (remainingCommand or "") veaf.loggers.get(veafShortcuts.Id):trace("command=%s", veaf.p(command)) local keywords = veaf.split(command, ",") local silent = false local zoneName = nil local password = nil for _, keyphrase in pairs(keywords) do local str = veaf.breakString(veaf.trim(keyphrase), " ") local key = str[1] local val = str[2] or "" if key:lower() == "silent" then silent = true end if key:lower() == "name" then zoneName = val end if key:lower() == "password" then password = val end end if not (bypassSecurity or veafSecurity.isAuthenticated()) then veaf.loggers.get(veafShortcuts.Id):trace("password=%s", veaf.p(password)) local hash = nil if password then hash = sha1.hex(password) end if not(self:hasPassword(hash)) then veaf.loggers.get(veafShortcuts.Id):warn("You have to give the correct alias password for %s to do this", self:getName()) trigger.action.outText("Please use the ', password ' option", 5) return false end end veaf.loggers.get(veafShortcuts.Id):trace("zoneName=%s", veaf.p(zoneName)) veaf.loggers.get(veafShortcuts.Id):trace("silent=%s", veaf.p(silent)) if not zoneName or #zoneName == 0 then local msg = string.format("VeafAliasForCombatZone: zone name is mandatory") veaf.loggers.get(veafShortcuts.Id):warn(msg) trigger.action.outText(msg, 5) return false end local zone = veafCombatZone.GetZone(zoneName) if not zone then local msg = string.format("VeafAliasForCombatZone: zone %s does not exist", veaf.p(zoneName)) veaf.loggers.get(veafShortcuts.Id):warn(msg) trigger.action.outText(msg, 5) return false end --veaf.loggers.get(veafShortcuts.Id):trace("zone=%s", veaf.p(zone)) if command:lower():sub(1, 5) == "start" then local result = veafCombatZone.ActivateZone(zoneName, silent) return result elseif command:lower():sub(1, 4) == "stop" then local result = veafCombatZone.DesactivateZone(zoneName, silent) return result end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- global functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- search for an alias function veafShortcuts.GetAlias(aliasName) veaf.loggers.get(veafShortcuts.Id):debug(string.format("veafShortcuts.GetAlias([%s])",aliasName or "")) veaf.loggers.get(veafShortcuts.Id):debug(string.format("Searching for alias with name [%s]", aliasName)) -- find the desired alias in the aliases list local alias = veafShortcuts.aliases[aliasName:lower()] if not alias then local message = string.format("VeafAlias [%s] was not found !",aliasName) veaf.loggers.get(veafShortcuts.Id):error(message) trigger.action.outText(message,5) end return alias end -- add an alias function veafShortcuts.AddAlias(alias) veaf.loggers.get(veafShortcuts.Id):debug(string.format("veafShortcuts.AddAlias([%s])",alias:getName() or "")) veafShortcuts.aliases[alias:getName():lower()] = alias return alias end -- execute an alias command function veafShortcuts.ExecuteAlias(aliasName, delay, remainingCommand, position, coalition, markId, bypassSecurity, spawnedGroups, route) veaf.loggers.get(veafShortcuts.Id):debug(string.format("veafShortcuts.ExecuteAlias([%s],[%s],[%s],[%s],[%s])", veaf.p(aliasName), veaf.p(delay), veaf.p(remainingCommand), veaf.p(position), veaf.p(coalition))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("markId=[%s]",veaf.p(markId))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("bypassSecurity=[%s]",veaf.p(bypassSecurity))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("route=[%s]",veaf.p(route))) local alias = veafShortcuts.GetAlias(aliasName) if alias then veaf.loggers.get(veafShortcuts.Id):trace(string.format("found VeafAlias[%s]",alias:getName() or "")) if alias:getBatchAliases() then -- no alias to actually execute, but instead run a batch -- the batch aliases are always password protected by a Mission Master password, so search for one local password = nil local keywords = veaf.split(remainingCommand, ",") for _, keyphrase in pairs(keywords) do local str = veaf.breakString(veaf.trim(keyphrase), " ") local key = str[1] local val = str[2] or "" if key:lower() == "password" then password = val end end if not (bypassSecurity or veafSecurity.isAuthenticated()) then veaf.loggers.get(veafShortcuts.Id):trace("password=%s", veaf.p(password)) local hash = nil if password then hash = sha1.hex(password) end if not(alias:hasPassword(hash)) then veaf.loggers.get(veafShortcuts.Id):warn("You have to give the correct alias password for %s to do this", alias:getName()) trigger.action.outText("Please use the ', password ' option", 5) return false end end local _msg = string.format("running batch alias [%s] : %s", alias:getName(), alias:getDescription()) veaf.loggers.get(veafShortcuts.Id):info(_msg) trigger.action.outText(_msg, 10) -- run the batch for index, textToExecute in ipairs(alias:getBatchAliases()) do veafShortcuts.executeCommand(position, textToExecute, coalition, markId, true, spawnedGroups, route) end else if delay and delay ~= "" then mist.scheduleFunction(VeafAlias.execute, {alias, remainingCommand, position, coalition, markId, bypassSecurity, spawnedGroups, route}, timer.getTime() + delay) else alias:execute(remainingCommand, position, coalition, markId, bypassSecurity, spawnedGroups, route) end end return true else veaf.loggers.get(veafShortcuts.Id):error(string.format("veafShortcuts.ExecuteAlias : cannot find alias [%s]",aliasName or "")) end return false end -- execute an alias command function veafShortcuts.ExecuteBatchAliasesList(aliasBatchList, delay, coalition, silent) veaf.loggers.get(veafShortcuts.Id):debug(string.format("veafShortcuts.ExecuteBatchAliasesList([%s],[%s],[%s],[%s])", veaf.p(aliasBatchList), veaf.p(delay), veaf.p(coalition), veaf.p(silent))) if aliasBatchList and #aliasBatchList > 0 then -- run a batch local _msg = string.format("running batch list [%s]", veaf.p(aliasBatchList)) veaf.loggers.get(veafShortcuts.Id):info(_msg) if not(silent) then trigger.action.outText(_msg, 10) end -- run the batch for index, textToExecute in ipairs(aliasBatchList) do veafShortcuts.executeCommand(nil, textToExecute, coalition, nil, true) end return true else veaf.loggers.get(veafShortcuts.Id):error(string.format("veafShortcuts.ExecuteBatchAliasesList : batch list is empty")) end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function executed when a mark has changed. This happens when text is entered or changed. function veafShortcuts.onEventMarkChange(eventPos, event) -- choose by default the coalition opposing the player who triggered the event local invertedCoalition = 1 if event.coalition == 1 then invertedCoalition = 2 end veaf.loggers.get(veafShortcuts.Id):trace(string.format("event.idx = %s", veaf.p(event.idx))) if veafShortcuts.executeCommand(eventPos, event.text, invertedCoalition, event.idx) then -- Delete old mark. veaf.loggers.get(veafShortcuts.Id):trace(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafShortcuts.executeCommand(eventPos, eventText, eventCoalition, markId, bypassSecurity, spawnedGroups, route) veaf.loggers.get(veafShortcuts.Id):debug("veafShortcuts.executeCommand(eventText=[%s])", eventText) -- Check if marker has a text and contains an alias if eventText ~= nil then -- Analyse the mark point text and extract the keywords. local alias, coords, delay, remainder = veafShortcuts.markTextAnalysis(eventText) if alias then local position = nil if coords and #coords > 0 then veaf.loggers.get(veafShortcuts.Id):trace("checking coords [%s]", coords) -- check for a known name (named point or trigger zone) local namedPoint = veafNamedPoints.getPoint(coords) if namedPoint then veaf.loggers.get(veafShortcuts.Id):trace("found named point [%s]", coords) position = namedPoint end -- check for a known zone (trigger zone) local triggerZone = veaf.getTriggerZone(coords) if triggerZone then veaf.loggers.get(veafShortcuts.Id):trace("found trigger zone [%s]=%s", coords, triggerZone) position = triggerZone end if not position then veaf.loggers.get(veafShortcuts.Id):trace("coords [%s] is not a known named point or trigger zone", coords) local _lat, _lon = veaf.computeLLFromString(coords) veaf.loggers.get(veafShortcuts.Id):trace("_lat=%s",_lat) veaf.loggers.get(veafShortcuts.Id):trace("_lon=%s",_lon) if _lat and _lon then position = coord.LLtoLO(_lat, _lon) veaf.loggers.get(veafShortcuts.Id):trace("position=%s",position) else local _msg = string.format("unable to decode coordinates [%s]", veaf.p(coords)) veaf.loggers.get(veafShortcuts.Id):warn(_msg) trigger.action.outText(_msg, 5) return end end else veaf.loggers.get(veafShortcuts.Id):trace("no coords found, using eventPos [%s]", eventPos) position = eventPos end -- do the magic return veafShortcuts.ExecuteAlias(alias, delay, remainder, position, eventCoalition, markId, bypassSecurity, spawnedGroups, route) end return false end -- None of the keywords matched. return false end --- Extract keywords from mark text. function veafShortcuts.markTextAnalysis(text) if text then veaf.loggers.get(veafShortcuts.Id):trace(string.format("veafShortcuts.markTextAnalysis(text=[%s])", text)) -- check for the alias starter if text:sub(1,1) == veafShortcuts.AliasStarter then veaf.loggers.get(veafShortcuts.Id):trace("found veafShortcuts.AliasStarter") -- extract alias and remainder local alias, coords, delay, remainder = text:match("(-[^#^!^ ^,]+)#?([^!^,^%s]*)!?(%d*)(.*)") veaf.loggers.get(veafShortcuts.Id):trace(string.format("alias=[%s]", veaf.p(alias))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("coords=[%s]", veaf.p(coords))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("delay=[%s]", veaf.p(delay))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("remainder=[%s]", veaf.p(remainder))) if alias then veaf.loggers.get(veafShortcuts.Id):trace(string.format("alias = [%s]", alias)) return alias, coords, delay, remainder end end end return nil end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- default aliases list ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafShortcuts.buildDefaultList() -- generic sam groups veafShortcuts.AddAlias( VeafAlias:new() :setName("-samLR") :setDescription("Random long range SAM battery") :setVeafCommand("_spawn samgroup, skynet true, spacing 1, radius 0") :addRandomParameter("defense", 4, 5) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-samSR") :setDescription("Random short range SAM battery") :setVeafCommand("_spawn samgroup, skynet true, spacing 1, radius 0") :addRandomParameter("defense", 2, 3) :setBypassSecurity(false) ) -- specific air defenses groups and units veafShortcuts.AddAlias( VeafAlias:new() :setName("-hq7") :setDescription("HQ-7 (Red Banner) battery") :setVeafCommand("_spawn group, name hq7, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hq7_single") :setDescription("HQ-7 (Red Banner) launcher") :setVeafCommand("_spawn group, name hq7_single, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hq7noew") :setDescription("HQ-7 (Red Banner) battery without EWR") :setVeafCommand("_spawn group, name hq7-noew, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hq7eo") :setDescription("HQ-7EO (Red Banner) battery") :setVeafCommand("_spawn group, name hq7eo, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hq7eo_single") :setDescription("HQ-7EO (Red Banner) launcher") :setVeafCommand("_spawn group, name hq7eo_single, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hq7eo_noew") :setDescription("HQ-7EO (Red Banner) battery without EWR") :setVeafCommand("_spawn group, name hq7eo-noew, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa2") :setDescription("SA-2 Guideline (S-75 Dvina) battery") :setVeafCommand("_spawn group, name sa2, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa5") :setDescription("SA-5 Gammon (S-200 Dubna) battery") :setVeafCommand("_spawn group, name sa5, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa3") :setDescription("SA-3 Goa (S-125 Neva/Pechora) battery") :setVeafCommand("_spawn group, name sa3, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa6") :setDescription("SA-6 Gainful (2K12 Kub) battery") :setVeafCommand("_spawn group, name sa6, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa8") :setDescription("SA-8 Osa (9K33 Osa) sam vehicle") :setVeafCommand("_spawn group, name sa8_squad, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa9") :setDescription("SA-9 Strela-1 (9K31 Strela-1) sam vehicle") :setVeafCommand("_spawn unit, name Strela-1 9P31") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa9_squad") :setDescription("SA-9 Strela-1 (9K31 Strela-1) sam vehicle and logistic") :setVeafCommand("_spawn group, name sa9_squad, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa10") :setDescription("SA-10 Grumble (S-300) battery") :setVeafCommand("_spawn group, name sa10, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa11") :setDescription("SA-11 Gadfly (9K37 Buk) battery") :setVeafCommand("_spawn group, name sa11, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa13") :setDescription("SA-13 Strela (9A35M3) sam vehicle") :setVeafCommand("_spawn unit, name Strela-10M3") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa13_squad") :setDescription("SA-13 Strela (9A35M3) sam vehicle and logistic") :setVeafCommand("_spawn group, name sa13_squad, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa15") :setDescription("SA-15 Gauntlet (9K330 Tor) sam vehicle") :setVeafCommand("_spawn group, name sa15_squad, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa15m2") :setDescription("SA-15M2 Gauntlet (9K330 TorM2) sam vehicle") :setVeafCommand("_spawn group, name sa15m2_squad, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa22") :setDescription("SA-22 Greyhound (Pantsir-S1) sam vehicle") :setVeafCommand("_spawn group, name sa22_squad, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-insurgent_manpad") :setDescription("Insurgent SA-18 manpad squad") :setVeafCommand("_spawn group, name ins_manpad") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa18") :setDescription("SA-18 manpad squad") :setVeafCommand("_spawn group, name sa18_squad") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa18s") :setDescription("SA-18S manpad squad") :setVeafCommand("_spawn group, name sa18s_squad") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa19") :setDescription("SA-19 Tunguska (2K22 Tunguska) sam vehicle and logistic") :setVeafCommand("_spawn group, name sa19_squad, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-shilka") :setDescription("ZSU-23-4 Shilka AAA vehicle") :setVeafCommand("_spawn unit, name ZSU-23-4 Shilka") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-zu23") :setDescription("ZU-23 AAA vehicle") :setVeafCommand("_spawn unit, name Ural-375 ZU-23") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-manpads") :setDescription("mutiple SA-18S manpad soldier peppered in a wide radius") :setVeafCommand("_spawn unit, name SA-18 Igla-S manpad, radius 5000") :addRandomParameter("multiplier", 3, 6) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-rapier") :setDescription("Rapier battery with Radar (US by default)") :setVeafCommand("_spawn group, name rapier_radar, country USA, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-roland") :setDescription("Roland battery with EWR (US by default)") :setVeafCommand("_spawn group, name roland, country USA, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-rolandnoew") :setDescription("Roland battery without EWR (US by default)") :setVeafCommand("_spawn group, name roland-noew, country USA, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-nasams") :setDescription("NASAMS battery with 120C (US by default)") :setVeafCommand("_spawn group, name nasams_c, country USA, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-nasams_b") :setDescription("NASAMS battery with 120B (US by default)") :setVeafCommand("_spawn group, name nasams_b, country USA, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hawk") :setDescription("Hawk battery (US by default)") :setVeafCommand("_spawn group, name hawk, country USA, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-patriot") :setDescription("Patriot battery (US by default)") :setVeafCommand("_spawn group, name patriot, country USA, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-stinger") :setDescription("Stinger manpad squad (US by default)") :setVeafCommand("_spawn group, name stinger_squad, country USA, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-avenger") :setDescription("Avenger SAM (US by default)") :setVeafCommand("_spawn unit, name avenger, country USA") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-avenger_squad") :setDescription("Avenger SAM (US by default) and logistic") :setVeafCommand("_spawn group, name avenger_squad, country USA, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-dogear") :setDescription("Dogear Radar") :setVeafCommand("_spawn unit, name dogear, skynet true, ewr") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-blue_ewr") :setDescription("F-117 Domed EWR (US by default)") :setVeafCommand("_spawn group, name blue_ewr, country USA, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-ewr") :setDescription("55G6 Mast EWR") :setVeafCommand("_spawn group, name ewr, skynet true, spacing 1, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-burke") :setDescription("USS Arleigh Burke IIa destroyer (US by default)") :setVeafCommand("_spawn unit, name USS_Arleigh_Burke_IIa, country USA") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-perry") :setDescription("O.H. Perry destroyer (US by default)") :setVeafCommand("_spawn unit, name PERRY, country USA") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-ticonderoga") :setDescription("Ticonderoga frigate (US by default)") :setVeafCommand("_spawn unit, name TICONDEROG, country USA") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-rezky") :setDescription("FF 1135M Rezky frigate (RU by default)") :setVeafCommand("_spawn unit, name REZKY, country RUSSIA") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-pyotr") :setDescription("CGN 1144.2 Pyotr Velikiy (RU by default)") :setVeafCommand("_spawn unit, name PIOTR, country RUSSIA") :setBypassSecurity(false) ) -- convoys veafShortcuts.AddAlias( VeafAlias:new() :setName("-hv_convoy_red") :setDescription("Red High Value Attack convoy") :setVeafCommand("_spawn group, name hv_convoy_red, country RUSSIA, skynet false, alarm 0") --Alarm is set to 0, meaning Alarm state 0 (AUTO) for proper movement of the scud :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-attack_convoy_red") :setDescription("Red Attack convoy") :setVeafCommand("_spawn group, name convoy_red, country RUSSIA, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-QRC_red") :setDescription("Quick Reaction Convoy red") --it's fast :setVeafCommand("_spawn group, name QRC_red, country RUSSIA, skynet true, speed 90, spacing 2") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-civilian_convoy_red") :setDescription("Red Civilian convoy") :setVeafCommand("_spawn group, name civilian_convoy_red, country RUSSIA") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-QRC_blue") :setDescription("Quick Reaction Convoy blue") --it's fast :setVeafCommand("_spawn group, name QRC_blue, country USA, skynet true, speed 90, spacing 2") :setBypassSecurity(false) ) -- shortcuts to commands veafShortcuts.AddAlias( VeafAlias:new() :setName("-point") :setDescription("Name a point on the map") :setVeafCommand("_name point") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the point name :setBypassSecurity(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-destroy") :setDescription("Destroy any unit within 100m") :setVeafCommand("_destroy, radius 100") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-login") :setDescription("Unlock the system") :setHidden(true) :setVeafCommand("_auth") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the password :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-logout") :setDescription("Lock the system") :setHidden(true) :setVeafCommand("_auth logout") :dontEndWithComma() :setBypassSecurity(false) ) -- shortcuts to specific groups veafShortcuts.AddAlias( VeafAlias:new() :setName("-mortar") :setDescription("Mortar team") :setVeafCommand("_spawn group, name mortar, country USA") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty") :setDescription("M-109 artillery battery") :setVeafCommand("_spawn group, name M-109, country USA") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-msta") :setDescription("Msta artillery battery") :setVeafCommand("_spawn group, name msta") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-plz05") :setDescription("PLZ-05 artillery battery") :setVeafCommand("_spawn group, name plz05") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-mlrs") :setDescription("MLRS artillery battery") :setVeafCommand("_spawn group, name mlrs, country USA") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-smerch_he") :setDescription("Smerch HE artillery battery") :setVeafCommand("_spawn group, name smerchhe") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-smerch_cm") :setDescription("Smerch CM artillery battery") :setVeafCommand("_spawn group, name smerchcm") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-uragan") :setDescription("Uragan artillery battery") :setVeafCommand("_spawn group, name uragan") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-grad") :setDescription("Grad artillery battery") :setVeafCommand("_spawn group, name grad") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-cargoships") :setDescription("Cargo ships") :setVeafCommand("_spawn group, name cargoships-nodef, country RUSSIA, offroad, speed 60, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-escortedcargoships") :setDescription("Cargo ships (escorted)") :setVeafCommand("_spawn group, name cargoships-escorted, country RUSSIA, offroad, speed 60, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-combatships") :setDescription("Combat ships") :setVeafCommand("_spawn group, name combatships, country RUSSIA, offroad, speed 60, skynet true") :setBypassSecurity(false) ) -- shortcuts to dynamic groups veafShortcuts.AddAlias( VeafAlias:new() :setName("-sam") :setDescription("Random SAM battery") :setVeafCommand("_spawn samgroup, skynet true, spacing 1, radius 0") :addRandomParameter("defense", 1, 5) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-aaa") :setDescription("Random AAA battery") :setVeafCommand("_spawn samgroup, skynet true, spacing 1, radius 0") :addRandomParameter("defense", 1, 2) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-armor") :setDescription("Dynamic armor group") :setVeafCommand("_spawn armorgroup") :addRandomParameter("defense", 1, 3) :addRandomParameter("armor", 2, 4) :addRandomParameter("size", 4, 8) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-infantry") :setDescription("Dynamic infantry section") :setVeafCommand("_spawn infantrygroup") :addRandomParameter("defense", 0, 5) :addRandomParameter("armor", 0, 5) :addRandomParameter("size", 4, 8) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-transport") :setDescription("Dynamic transport company") :setVeafCommand("_spawn transportgroup") :addRandomParameter("defense", 0, 3) :addRandomParameter("size", 10, 25) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-combat") :setDescription("Dynamic combat group") :setVeafCommand("_spawn combatgroup") :addRandomParameter("defense", 1, 3) :addRandomParameter("armor", 2, 4) :addRandomParameter("size", 1, 4) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-cas") :setDescription("Generate a random CAS group for training") :setVeafCommand("_cas, disperse") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-cargo") :setDescription("Generate a cargo for sling loading") :setVeafCommand("_spawn cargo, side blue, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-refuel") :setDescription("spawns an US refuel group") :setVeafCommand("_spawn group, name US-refuel, country usa, radius 0") :setBypassSecurity(false) ) -- radio shortcuts veafShortcuts.AddAlias( VeafAlias:new() :setName("-send") :setDescription("Send radio message - needs \"MESSAGE\"") :setVeafCommand("_radio transmit, message") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the message content :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-play") :setDescription("Play sound over radio - needs \"FILENAME\"") :setVeafCommand("_radio play, path") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the message content :setBypassSecurity(false) ) -- other shortcuts veafShortcuts.AddAlias( VeafAlias:new() :setName("-convoy") :setDescription("Convoy - needs \", dest POINTNAME\"") :setVeafCommand("_spawn convoy") :addRandomParameter("defense", 0, 3) :addRandomParameter("armor", 0, 4) :addRandomParameter("size", 6, 15) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-jtac") :setDescription("JTAC humvee") :setVeafCommand("_spawn jtac") :setBypassSecurity(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-afac") :setDescription("AFAC MQ-9 Reaper") :setVeafCommand("_spawn afac") :setBypassSecurity(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-afachere") :setDescription("move an afac to a specific location ; must follow with the afac group name ; can also set speed, alt and hdg") :setVeafCommand("_move afac, name") :dontEndWithComma() :setBypassSecurity(false) ) -- Ground unit AI veafShortcuts.AddAlias( VeafAlias:new() :setName("-ai_set") :setDescription("Sets an AI handler for a group of units") :setVeafCommand("_ground set") ) -- ARTY-1 commands veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty1") :setDescription("Spawns ARTY-1") :setBatchAliases({"-arty, unitname arty-1", "-ai_set, name arty-1, groupname arty-1"}) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty1_aim") :setDescription("Orders ARTY-1 to fire for aim") :setVeafCommand("_ground order, name arty-1, order aim;radius 15-30; target") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the target coordinates ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty1_fire") :setDescription("Orders ARTY-1 to fire for effect") :setVeafCommand("_ground order, name arty-1, order fire; radius 50-150; shells 40-80; target") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the target coordinates ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty1_stop") :setDescription("Orders ARTY-1 to stop listening for orders") :setVeafCommand("_ground stop, name arty-1") ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty1_start") :setDescription("Orders ARTY-1 to start listening for orders") :setVeafCommand("_ground start, name arty-1") ) -- ARTY-2 commands veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty2") :setDescription("Spawns ARTY-2") :setBatchAliases({"-arty, unitname arty-2", "-ai_set, name arty-2, groupname arty-2"}) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty2_aim") :setDescription("Orders ARTY-2 to fire for aim") :setVeafCommand("_ground order, name arty-2, order aim;radius 15-30; target") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the target coordinates ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty2_fire") :setDescription("Orders ARTY-2 to fire for effect") :setVeafCommand("_ground order, name arty-2, order fire; radius 50-150; shells 40-80; target") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the target coordinates ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty2_stop") :setDescription("Orders ARTY-2 to stop listening for orders") :setVeafCommand("_ground stop, name arty-2") ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty2_start") :setDescription("Orders ARTY-2 to start listening for orders") :setVeafCommand("_ground start, name arty-2") ) -- ARTY-3 commands veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty3") :setDescription("Spawns ARTY-3") :setBatchAliases({"-arty, unitname arty-3", "-ai_set, name arty-3, groupname arty-3"}) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty3_aim") :setDescription("Orders ARTY-3 to fire for aim") :setVeafCommand("_ground order, name arty-3, order aim;radius 15-30; target") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the target coordinates ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty3_fire") :setDescription("Orders ARTY-3 to fire for effect") :setVeafCommand("_ground order, name arty-3, order fire; radius 50-150; shells 40-80; target") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the target coordinates ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty3_stop") :setDescription("Orders ARTY-3 to stop listening for orders") :setVeafCommand("_ground stop, name arty-3") ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arty3_start") :setDescription("Orders ARTY-3 to start listening for orders") :setVeafCommand("_ground start, name arty-3") ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-cesar") :setDescription("Artillery precision shelling of a zone with a few low-yield HE") :setVeafCommand("_spawn bomb") :addRandomParameter("shells", 2, 5) :addRandomParameter("radius", 15, 30) :addRandomParameter("power", 10, 50) :setHidden(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-shell") :setDescription("Artillery shelling of a small zone with lots of low-yield HE") :setVeafCommand("_spawn bomb") :addRandomParameter("shells", 2, 5) :addRandomParameter("radius", 100, 300) :addRandomParameter("power", 10, 50) :addRandomParameter("multiplier", 5, 10) :setHidden(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-flak") :setDescription("Anti-air Artillery shelling of a zone with flak") :setVeafCommand("_spawn bomb, alt 6000") :addRandomParameter("shells", 10, 15) :addRandomParameter("radius", 1000, 1500) :addRandomParameter("power", 500, 750) :addRandomParameter("altdelta", 800, 1000) :addRandomParameter("multiplier", 6, 10) :setHidden(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-light") :setDescription("Illumination by artillery shelling of a zone") :setVeafCommand("_spawn flare, radius 1500") :addRandomParameter("shells", 10, 15) :setBypassSecurity(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-smoke") :setDescription("Spawn a single white smoke") :setVeafCommand("_spawn smoke, color white, shells 1, radius 1") :setBypassSecurity(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-longsmoke") :setDescription("Spawn a single white smoke, renewed every 5 minutes for 30 minutes") :setVeafCommand("_spawn smoke, color white, shells 1, radius 1, repeat 5, delay 300") :setBypassSecurity(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-signal") :setDescription("Spawn a single signal flare") :setVeafCommand("_spawn signal, color green, shells 1, radius 1") :setBypassSecurity(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-tankerhere") :setDescription("move a tanker to a specific location ; must follow with the tanker group name ; can also set speed, alt, hdg and distance") :setVeafCommand("_move tanker, teleport, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-tanker") :setDescription("alias for '-tankerhere'") :setVeafCommand("-tankerhere") ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-tankerlow") :setDescription("sets the closest tanker to FL120 at 200 KIAS") :setVeafCommand("_move tankermission, alt 12000, speed 250") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-tankerhigh") :setDescription("sets the closest tanker to FL220 at 300 KIAS") :setVeafCommand("_move tankermission, alt 22000, speed 450") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-tacan") :setDescription("create a portable TACAN beacon") :setVeafCommand("_spawn tacan, band X, channel 99") :setBypassSecurity(true) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-farp") :setDescription("create a new FARP") :setVeafCommand("_spawn farp, side blue, radius 0, name ") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the FARP name :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-farpNoMarker") :setDescription("create a new invisible FARP, skipping the special vehicles that mark its position") :setVeafCommand("_spawn farp, side blue, radius 0, noFarpMarkers, name ") :dontEndWithComma() -- !! don't end with a comma, because we'd break setting the FARP name :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-fob") :setDescription("create a new FOB") :setVeafCommand("_spawn fob, side blue, radius 0") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-draw") :setDescription("start a drawing on the map, or add a point to an existing drawing ; name is mandatory") :setVeafCommand("_drawing add, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-arrow") :setDescription("start drawing an arrow on the map, or add a point to an existing arrow ; name is mandatory") :setVeafCommand("_drawing add, arrow, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-square") :setDescription("add a square to the map; name is mandatory") :setVeafCommand("_drawing square, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-circle") :setDescription("add a circle to the map; name is mandatory") :setVeafCommand("_drawing circle, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-erasedrawing") :setDescription("erase a drawing from the map ; name is mandatory") :setVeafCommand("_drawing erase, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-cap") :setDescription("Dynamic combat air patrol") :setVeafCommand("_spawn cap, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-flag") :setDescription("Mission Master : get flag value") :setVeafCommand("_mm getflag, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-flagon") :setDescription("Mission Master : set flag value to ON") :setVeafCommand("_mm flagon, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-flagoff") :setDescription("Mission Master : set flag value to OFF") :setVeafCommand("_mm flagoff, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-run") :setDescription("Mission Master : run runnable") :setVeafCommand("_mm run, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAliasForCombatMission:new() :setName("-airstart") :setDescription("Run a combat mission") :setVeafCommand("start, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAliasForCombatMission:new() :setName("-airstop") :setDescription("Stop a combat mission") :setVeafCommand("stop, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAliasForCombatZone:new() :setName("-zonestart") :setDescription("Activate a combat zone") :setVeafCommand("start, name") :dontEndWithComma() :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAliasForCombatZone:new() :setName("-zonestop") :setDescription("Desactivate a combat zone") :setVeafCommand("stop, name") :dontEndWithComma() :setBypassSecurity(false) ) end function veafShortcuts.dumpAliasesList(export_path) local jsonify = function(key, value) veaf.loggers.get(veafShortcuts.Id):trace(string.format("jsonify(%s)", veaf.p(value))) if veaf.json then return veaf.json.stringify(veafShortcuts.GetAlias(value)) else return "" end end -- sort the aliases alphabetically local sortedAliases = {} for _, alias in pairs(veafShortcuts.aliases) do table.insert(sortedAliases, alias:getName()) end table.sort(sortedAliases) veaf.loggers.get(veafShortcuts.Id):trace(string.format("sortedAliases=%s", veaf.p(sortedAliases))) local _filename = "AliasesList.json" if veaf.config.MISSION_NAME then _filename = "AliasesList_" .. veaf.config.MISSION_NAME .. ".json" end if not(veaf.DO_NOT_EXPORT_JSON_FILES) then veaf.exportAsJson(sortedAliases, "aliases", jsonify, _filename, export_path or veaf.config.MISSION_EXPORT_PATH) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- execute command from the remote interface function veafShortcuts.executeCommandFromRemote(parameters) veaf.loggers.get(veafShortcuts.Id):debug(string.format("veafShortcuts.executeCommandFromRemote()")) veaf.loggers.get(veafShortcuts.Id):trace(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) veaf.loggers.get(veafShortcuts.Id):trace(string.format("_pilot= %s", veaf.p(_pilot))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("_unitName= %s", veaf.p(_unitName))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("_command= %s", veaf.p(_command))) if not _pilot or not _command then return false end if _command then local _lat, _lon, _alias = nil, nil, nil local _coa = coalition.side.BLUE local _unit = Unit.getByName(_unitName) if _unit then _coa = _unit:getCoalition() veaf.loggers.get(veafShortcuts.Id):trace("_coa=s",veaf.p(_coa)) end -- choose by default the coalition opposing the player who triggered the event local invertedCoalition = 1 if _coa == 1 then invertedCoalition = 2 end if _command:sub(1,1) == veafShortcuts.AliasStarter or _command:sub(2,2) == veafShortcuts.AliasStarter then -- there is only the command _alias = _command veaf.loggers.get(veafShortcuts.Id):trace(string.format("_alias=%s",veaf.p(_alias))) else -- parse the command local _coords, __alias = _command:match(veafShortcuts.RemoteCommandParser) _alias = __alias veaf.loggers.get(veafShortcuts.Id):trace(string.format("_coords=%s",veaf.p(_coords))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("_alias=%s",veaf.p(_alias))) if _coords then _lat, _lon = veaf.computeLLFromString(_coords) veaf.loggers.get(veafShortcuts.Id):trace(string.format("_lat=%s",veaf.p(_lat))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("_lon=%s",veaf.p(_lon))) end end if _alias then if _lat and _lon then local _pos = coord.LLtoLO(_lat, _lon) veaf.loggers.get(veafShortcuts.Id):trace(string.format("_pos=%s",veaf.p(_pos))) veaf.loggers.get(veafShortcuts.Id):trace(string.format("_coa=%s",veaf.p(_coa))) veaf.loggers.get(veafShortcuts.Id):info(string.format("[%s] is running an alias at position [%s] for coalition [%s] : [%s]",veaf.p(_pilot.name), veaf.p(_pos), veaf.p(_coa), veaf.p(_alias))) veafShortcuts.executeCommand(_pos, _alias, invertedCoalition, _pilot.name) return true else veaf.loggers.get(veafShortcuts.Id):info(string.format("[%s] is running an alias with no specific position for coalition [%s] : [%s]",veaf.p(_pilot.name), veaf.p(_coa), veaf.p(_alias))) veafShortcuts.executeCommand(nil, _alias, invertedCoalition, _pilot.name) end end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafShortcuts.initialize() veaf.loggers.get(veafShortcuts.Id):info("Initializing module") veafShortcuts.buildDefaultList() veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafShortcuts.onEventMarkChange) veafShortcuts.dumpAliasesList() end veaf.loggers.get(veafShortcuts.Id):info(string.format("Loading version %s", veafShortcuts.Version)) ------------------ END script veafShortcuts.lua ------------------ ------------------ START script veafAirbases.lua ------------------ ------------------------------------------------------------------ -- VEAF airbases information -- By Flogas (2024) -- -- Features: -- --------- -- * Extraction and normalization of airbase an runway data from DCS APIs ------------------------------------------------------------------ veafAirbases = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global module settings ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafAirbases.Id = "AIRBASES" --- Version. veafAirbases.Version = "1.1.0" -- trace level, specific to this module --veafAirbases.LogLevel = "trace" veaf.loggers.new(veafAirbases.Id, veafAirbases.LogLevel) veafAirbases.Airbases = nil ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Local constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- local _manualRunwayNumberCorrections = { -- Syria -- ["Beirut-Rafic Hariri"] = { [34] = 3, [168] = 16 }, -- Correct RWYs: 17/35, 03/21, 16/34 ; API gives: RWY17 for 178° (ok), RWY35 for 034°, RWY03 for 168° ["Khalkhalah"] = { [151] = 15, [76] = 7 }, -- Correct RWYs: 15/33, 07/25 ; API gives: RWY03 for 151°, RWY03 for 076° ; note: runways have no painted numbers, 15/33 in DCS F10 airport window ["Minakh"] = { [42] = -1}, -- Correct RWY: 10/28 ; API gives: RWY10 for 101° (ok), RWY28 for 042° ; note: runway oriented to 042° is closed ["Ramat David"] = { [110] = 11, [88] = 9 }, -- Correct RWYs: 15/33, 11/29, 09/27 ; API gives: RWY15 for 146° (ok), RWY33 for 110°, RWY11 for 088° ["Nicosia"] = { [91] = 9 }, -- Correct RWYs: 14/32, 09/27 ; API gives: RWY32 for 328° (ok), RWY14 for 091° ["H3"] = { [64] = 6 }, -- Correct RWYs: 11/29, 06/24 ; API gives: RWY11 for 111° (ok), RWY29 for 064° ["Muwaffaq Salti"] = { [312] = 31 }, -- Correct RWYs: 08/26, 13/31 ; API gives: RWY26 for 261° (ok), RWY08 for 312° ["Tel Nof"] = { [184] = 18, [332] = 33 }, -- Correct RWYs: 15/33(2), 18/36 ; API gives: RWY33 for 332° (ok), RWY15 for 184°, RWY18 for 332° ["Ben Gurion"] = { [210] = 21, [262] = 26 }, -- Correct RWYs: 12/30, 03/21, 08/26 ; API gives: RWY12 for 123° (ok), RWY30 for 210°, RWY21 for 262° ["Hatzor"] = { [113] = 11 }, -- Correct RWYs: 05/23, 11/29(2) ; API gives: RWY23 for 236° (ok), RWY05 for 113°, RWY11 for 293° (ok) -- Nevada -- ["Creech"] = { [145] = 13 }, -- Correct RWYs: 08/26, 13/31 ; API gives: RWY08 for 091° (ok), RWY26 for 145° ["Boulder City"] = { [278] = 27 }, -- Correct RWYs: 15/33, 09/27 ; API gives: RWY33 for 344° (ok), RWY15 for 278° ["North Las Vegas"] = { [313] = 30 }, -- Correct RWYs: 07/25, 12/30(2) ; API gives: RWY25 for 267° (ok), RWY07 for 313°, RWY30 for 133° (ok) ["Tonopah"] = { [345] = 33 }, -- Correct RWYs: 11/29, 15/33 ; API gives: RWY29 for 305° (ok), RWY11 for 345° } ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Static methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafAirbases.initialize(bReset) bReset = bReset or false if (bReset) then veafAirbases.Airbases = nil end if (veafAirbases.Airbases) then return end veafAirbases.Airbases = {} local dcsAirBases = world.getAirbases() for i = 1, #dcsAirBases do local dcsAirbase = dcsAirBases[i] --[[ local s = string.format("%s %s %s %s", veaf.p(dcsAirbase:getCallsign()), veaf.p(dcsAirbase:getUnit()), veaf.p(dcsAirbase:getID()), veaf.p(dcsAirbase:getCategoryEx()) ) veaf.loggers.get(veafAirbases.Id):trace(s) veaf.loggers.get(veafAirbases.Id):trace(veaf.p(dcsAirbase:getDesc())) ]] local veafAirbase = veafAirbase:create(dcsAirbase) if (veafAirbase) then table.insert(veafAirbases.Airbases, veafAirbase) end end veaf.loggers.get(veafAirbases.Id):trace("Airbases and runways initialized for theater " .. env.mission.theatre) end function veafAirbases.getAirbaseByName(sAirbaseName) veafAirbases.initialize() for _, veafAirbase in pairs(veafAirbases.Airbases) do if (veafAirbase.Name == sAirbaseName) then return veafAirbase end end return nil end function veafAirbases.getAirbaseFromDcsAirbase(dcsAirbase) if (dcsAirbase == nil) then return nil end return veafAirbases.getAirbaseByName(dcsAirbase:getName()) end function veafAirbases.getNearestAirbaseList(dcsUnit, iCount) veafAirbases.initialize() iCount = iCount or 1 local vec3Unit = dcsUnit:getPoint() local iMinDistance = nil local nearestList = {} local function Sort(a, b) if (a == nil and b == nil) then return false elseif (a == nil) then return false elseif (b == nil) then return true else return a[2] < b[2] end end for _, veafAirbase in pairs(veafAirbases.Airbases) do local vec3Airbase = veafAirbase.DcsAirbase:getPoint() local iDistance = mist.utils.get2DDist(vec3Unit, vec3Airbase) local bAdded = false -- first fill all the nil positions for i = 1, iCount, 1 do if (nearestList[i] == nil) then nearestList[i] = { veafAirbase, iDistance } bAdded = true break end end if (not bAdded) then -- then, replace the farthest one if the current one is closer for i = iCount, 1, -1 do if (iDistance < nearestList[i][2]) then nearestList[i] = { veafAirbase, iDistance} bAdded = true break end end end if (bAdded) then table.sort(nearestList, Sort) end end return nearestList end function veafAirbases.getNearestAirbase(dcsUnit) local nearestList = veafAirbases.getNearestAirbaseList(dcsUnit, 1) if (nearestList and #nearestList >= 1) then local veafAirbase = nearestList[1][1] veaf.loggers.get(veafAirbases.Id):trace(string.format("Nearest airbase for [ %s ]: [ %s ] at %dm", dcsUnit:getName(), veafAirbase:toString(), nearestList[1][2])) return nearestList[1][1] else veaf.loggers.get(veafAirbases.Id):trace(string.format("No near airbase for [ %s ]", dcsUnit:getName())) return nil end end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- Airbase descriptor class --- Holds normalized information describing a DCS airbase --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veafAirbase = {} veafAirbase.__index = veafAirbase --------------------------------------------------------------------------------------------------- --- CTOR function veafAirbase:create(dcsAirbase) if (dcsAirbase == nil) then return nil end local sDisplayName = dcsAirbase:getName() local iCategory = dcsAirbase:getCategoryEx() local sTypeName = dcsAirbase:getTypeName() if (iCategory == Airbase.Category.SHIP and string.find(sTypeName, "FARP")) then iCategory = Airbase.Category.HELIPAD -- remediation for "FARP_SINGLE_01" or "VAP FARP" which are miscategorized end if (iCategory == Airbase.Category.SHIP) then local sShipType = sTypeName local dcsUnit = dcsAirbase:getUnit() if (dcsUnit) then local dcsUnitDesc = dcsUnit:getDesc() local sDcsUnitType = dcsUnit:getTypeName() if (dcsUnitDesc and not veaf.isNullOrEmpty(dcsUnitDesc.displayName)) then sShipType = dcsUnitDesc.displayName elseif (not veaf.isNullOrEmpty(sDcsUnitType)) then sShipType = sDcsUnitType end end sDisplayName = string.format("%s(%s)", sDisplayName, sShipType) --[[ if (dcsUnit) then local dcsUnitName = dcsUnit:getName() local dcsUnitType = dcsUnit:getTypeName() veaf.loggers.get(veafAirbases.Id):trace(string.format(">>>>>>> unit - %s - type=%s", dcsUnitName, dcsUnitType)) veaf.loggers.get(veafAirbases.Id):trace(veaf.p(dcsUnit:getDesc())) local desc = dcsUnit:getDesc() veaf.loggers.get(veafAirbases.Id):trace(desc.Kmax) veaf.loggers.get(veafAirbases.Id):trace(desc.displayName) end ]] elseif (iCategory == Airbase.Category.HELIPAD) then sDisplayName = sDisplayName .. "(FARP)" end veaf.loggers.get(veafAirbases.Id):trace(string.format(">> Airbase - %s created with display name %s", dcsAirbase:getName(), sDisplayName)) local veafRunways = {} if (iCategory == Airbase.Category.AIRDROME) then local dcsRunways = dcsAirbase:getRunways() for iRunwayReportOrder, dcsRunway in pairs(dcsRunways) do local veafRunway = veafAirbaseRunway:create(dcsAirbase, dcsRunway, iRunwayReportOrder) if (veafRunway) then table.insert(veafRunways, veafRunway) end end end local this = { Name = dcsAirbase:getName(), DisplayName = sDisplayName, Category = iCategory, DcsAirbase = dcsAirbase, Runways = veafRunways } setmetatable(this, veafAirbase) return this end --------------------------------------------------------------------------------------------------- --- Methods function veafAirbase:getRunwayInService(iWindDirectionTrue) local function _getHeadwind(iWindDirection, nRunwayHeading) local nAngle = math.abs(iWindDirection - nRunwayHeading) if (nAngle > 180) then nAngle = 360 - nAngle end return math.cos(math.rad(nAngle)) end local bestRunwayEnd = nil local nBestHeadwind = -math.huge -- Start with lowest possible value for i, veafRunway in ipairs(self.Runways) do for _, veafRunwayEnd in ipairs(veafRunway) do local nHeadwind = _getHeadwind(iWindDirectionTrue, veafRunwayEnd.Heading) if nHeadwind > nBestHeadwind then nBestHeadwind = nHeadwind bestRunwayEnd = veafRunwayEnd end end end local sLog = string.format("Runway in service for [ %s ] with wind from [ %03dT ] -->", self:toString(), iWindDirectionTrue) if (bestRunwayEnd) then sLog = sLog .. string.format(" [ %02d ]", bestRunwayEnd.Number) else sLog = sLog .. " none identified" end veaf.loggers.get(veafAirbases.Id):trace(sLog) return bestRunwayEnd end function veafAirbase:getRunwayInServiceString(iWindDirectionTrue) local veafRunwayEnd = self:getRunwayInService(iWindDirectionTrue) if (veafRunwayEnd) then return string.format("%02d", veafRunwayEnd.Number) else return nil end end function veafAirbase:toString() local s = self.Name for _, veafRunway in pairs(self.Runways) do s = s .. string.format(" | %s", veafRunway:toString()) end return s end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- Runway descriptor class --- Holds normalized information describing a DCS airbase runway --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veafAirbaseRunway = {} veafAirbaseRunway.__index = veafAirbaseRunway --------------------------------------------------------------------------------------------------- --- CTOR function veafAirbaseRunway:create(dcsAirbase, dcsRunway, iReportOrder) if (dcsAirbase == nil or dcsRunway == nil) then return nil end local function _normalizeHeading(nHeading) nHeading = nHeading % 360 if (nHeading == 0) then nHeading = 360 end return nHeading end local function _normalizeNumber(iNumber) iNumber = iNumber % 36 if (iNumber == 0) then iNumber = 36 end return iNumber end local function _numberFromHeading(nHeading) return mist.utils.round(nHeading / 10) end local function _numbersOffest(iOffset1, iOffest2) -- Calculate the absolute difference between angles local iOffset = math.abs(iOffset1 - iOffest2) local iOffsetOpposite = math.abs(36 - iOffset) return math.min(iOffset, iOffsetOpposite) end local sAirbaseName = dcsAirbase:getName() local iDcsNumber = tonumber(dcsRunway.Name) local nDcsHeading = _normalizeHeading(math.deg(-dcsRunway.course)) -- correction for special misreported runways -- first reported runway is assumed always ok -- and we need to assume that since sometimes it has the same reported heading as another incorrect one (Tel Nof in Syria for ex) if (iReportOrder and iReportOrder > 1) then local manualAirbaseCorrections = _manualRunwayNumberCorrections[sAirbaseName] if (manualAirbaseCorrections) then local iCorrectedNumber = manualAirbaseCorrections[math.floor(nDcsHeading)] if (iCorrectedNumber and iCorrectedNumber > 0) then iDcsNumber = iCorrectedNumber elseif (iCorrectedNumber and iCorrectedNumber <= 0) then return nil -- for closed runways end end end --[[ if (sAirbaseName == "Pahute Mesa") then veaf.loggers.get(veafAirbases.Id):trace("=======") veaf.loggers.get(veafAirbases.Id):trace(string.format("%d, %.2f", iDcsNumber, nDcsHeading)) veaf.loggers.get(veafAirbases.Id):trace(veaf.p(dcsRunway)) end ]] if (iDcsNumber == nil) then iDcsNumber = _numberFromHeading(nDcsHeading - veaf.getMagneticDeclination()) end -- Calculate standard runway number (first two digits of heading) local iNumberFromHeading = _numberFromHeading(nDcsHeading) -- Calculate the opposite heading local nOppositeHeading = _normalizeHeading(nDcsHeading + 180) local iOppositeNumberFromHeading = _numberFromHeading(nOppositeHeading) local iPrimaryNumber = iDcsNumber local iSecondaryNumber = _normalizeNumber(iDcsNumber + 18) local nPrimaryHeading, nSecondaryHeading local iFlipThreshold = 4 if (_numbersOffest(iDcsNumber, iOppositeNumberFromHeading) <= iFlipThreshold or _numbersOffest(iDcsNumber, iNumberFromHeading) > iFlipThreshold) then -- If DCS number if close to computed opposite heading, or far from the DCS heading, flip the heading nPrimaryHeading = nOppositeHeading nSecondaryHeading = nDcsHeading else nPrimaryHeading = nDcsHeading nSecondaryHeading = nOppositeHeading end -- order the runway in ascending numbers local iNumber1 = iPrimaryNumber local nHeading1 = nPrimaryHeading local iNumber2 = iSecondaryNumber local nHeading2 = nSecondaryHeading if (iPrimaryNumber > iSecondaryNumber) then iNumber1 = iSecondaryNumber nHeading1 = nSecondaryHeading iNumber2 = iPrimaryNumber nHeading2 = nPrimaryHeading end local this = { [1] = { Number = iNumber1, Heading = nHeading1 }, [2] = { Number = iNumber2, Heading = nHeading2 }, } setmetatable(this, veafAirbaseRunway) return this end --------------------------------------------------------------------------------------------------- --- Methods function veafAirbaseRunway:toString() return string.format("RWY %02d(%.2fT) / %02d(%.2fT)", self[1].Number, self[1].Heading, self[2].Number, self[2].Heading) end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- MODULE TESTS --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --[[ veafAirbases.initialize() veaf.loggers.get(veafAirbases.Id):trace("Airbases and runways initialized for theater " .. env.mission.theatre) for _, veafAirbase in pairs(veafAirbases.Airbases) do veaf.loggers.get(veafAirbases.Id):trace(veafAirbase:toString()) end ]] ------------------ END script veafAirbases.lua ------------------ ------------------ START script veafAirWaves.lua ------------------ ------------------------------------------------------------------ -- VEAF Air Waves for DCS World -- By Zip (2023) -- -- Features: -- --------- -- * Define zones that are defended by waves of AI flights -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafAirWaves = {} --- Identifier. All output in the log will start with this. veafAirWaves.Id = "AIRWAVES - " --- Version. veafAirWaves.Version = "1.7.7" -- trace level, specific to this module --veafAirWaves.LogLevel = "trace" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veaf.loggers.new(veafAirWaves.Id, veafAirWaves.LogLevel) veafAirWaves.zones = {} veafAirWaves.WATCHDOG_DELAY = 1 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- AirWave class methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- AirWaveZone = {} function AirWaveZone.init(object) -- technical name (AirWave instance name) object.name = nil -- description for the messages object.description = nil -- trigger zone name (if set, we'll use a DCS trigger zone) object.triggerZoneName = nil -- center (point in the center of the circle, when not using a DCS trigger zone) object.zoneCenter = nil -- radius (size of the circle, when not using a zone) - in meters object.zoneRadius = nil -- draw the zone on screen object.drawZone = false -- default position for respawns (im meters, lat/lon, relative to the zone center) object.respawnDefaultOffset = {latDelta=0, lonDelta=0} -- radius of the waves groups spawn object.respawnRadius = 250 -- coalitions of the players (only human units from these coalitions will be monitored) object.playerCoalitions = {} -- player units (if they die, reset the zone) object.playerUnitsNames = {} -- aircraft groups forming the waves object.waves = {} -- groups that have been spawned (the current wave) object.spawnedGroupsNames = {} -- silent means no message is emitted object.silent = false -- message when the zone is activated object.messageStart = veafAirWaves.DEFAULT_MESSAGE_START -- event when the zone is activated object.onStart = nil -- message when the zone is waiting for more players object.messageWaitForHumans = veafAirWaves.DEFAULT_MESSAGE_WAIT_FOR_HUMANS -- event when the zone is waiting for more players object.onWaitForHumans = nil -- message when a wave will be triggered object.messageWaitToDeploy = veafAirWaves.DEFAULT_MESSAGE_WAIT_TO_DEPLOY -- event when a wave will be triggered object.onWaitToDeploy = nil -- message when a wave is triggered object.messageDeploy = veafAirWaves.DEFAULT_MESSAGE_DEPLOY -- message to each players in the zone when a wave is triggered object.messageDeployPlayers = veafAirWaves.DEFAULT_MESSAGE_DEPLOY_PLAYERS -- event when a wave is triggered object.onDeploy = nil -- message when a player is outside of zone object.messageOutsideOfZone = veafAirWaves.DEFAULT_MESSAGE_OUTSIDE_OF_ZONE_PLAYERS -- event when a player is outside of zone object.onOutsideOfZone = nil -- message when a wave is destroyed object.messageDestroyed = veafAirWaves.DEFAULT_MESSAGE_DESTROYED -- event when a wave is destroyed object.onDestroyed = nil -- message when all waves are finished object.messageWon = veafAirWaves.DEFAULT_MESSAGE_WON -- event when all waves are finished object.onWon = nil -- message when the zone is lost object.messageLost = veafAirWaves.DEFAULT_MESSAGE_LOST -- event when all players are dead object.onLost = nil -- message when the zone is deactivated object.messageStop = veafAirWaves.DEFAULT_MESSAGE_STOP -- event when the zone is deactivated object.onStop = nil -- default delay in seconds between waves of enemy planes object.delayBetweenWaves = 0 -- the delay after this wave, and before the next one (either set in the wave definition, or it's the default delayBetweenWaves) object.delayBeforeNextWave = nil -- the time when the next wave is supposed to spawn (used to know when to actually spawn when in the STATUS_WAITING_FOR_NEXTWAVE state) object.timeOfNextWave = nil -- delay in seconds between the first human in zone and the actual activation of the zone object.delayBeforeActivation = 0 -- the time when the zones is supposed to be activated (used to know when to actually activate when in the STATUS_WAITING_FOR_MORE_HUMANS state) object.timeOfActivation = nil -- if true, the zone will reset when player dies object.resetWhenDying = true -- human units that are being watched object.playerHumanUnits = nil -- names of the human units that are being watched object.playerHumanUnitsNames = nil -- IA units that are being watched object.unitsInZone = {} -- players in the zone will only be detected below this altitude (in feet) object.minimumAltitude = -999999 -- players in the zone will only be detected above this altitude (in feet) object.maximumAltitude = 999999 -- players staying out of the zone for more that this number of seconds will be destroyed object.maxSecondsOutsideOfZonePlayers = veafAirWaves.MAX_SECONDS_OUTSIDE_OF_ZONE_PLAYERS -- IA staying out of the zone for more that this number of seconds will be destroyed object.maxSecondsOutsideOfZoneIA = veafAirWaves.MAX_SECONDS_OUTSIDE_OF_ZONE_IA -- the function that decides if a wave is dead or not (as a set of groups and units) object.isEnemyWaveDeadCallback = AirWaveZone.isEnemyWaveDead -- the function that decides if IA ennemy groups are dead (individually) object.isEnemyGroupDeadCallback = AirWaveZone.isEnemyGroupDead -- the minimum percentage of life that an AI unit is supposed to have to be considered alive object.minimumLifeForAiInPercent = veafAirWaves.MINIMUM_LIFE_FOR_AI_IN_PERCENT -- the function that handles crippled enemy units object.handleCrippledEnemyUnitCallback = AirWaveZone.handleCrippledEnemyUnit -- current wave number object.currentWaveIndex = 0 -- the drawing object that has been used to draw the zone object.zoneDrawing = nil -- the scheduled state of the :check() function object.checkFunctionSchedule = nil -- the time humans exited the zone object.timestampsOutOfZone = {} end function veafAirWaves.statusToString(status) if status == veafAirWaves.STATUS_READY then return "STATUS_READY" end if status == veafAirWaves.STATUS_WAITING_FOR_MORE_HUMANS then return "STATUS_WAITING_FOR_MORE_HUMANS" end if status == veafAirWaves.STATUS_ACTIVE then return "STATUS_ACTIVE" end if status == veafAirWaves.STATUS_WAITING_FOR_NEXTWAVE then return "STATUS_WAITING_FOR_NEXTWAVE" end if status == veafAirWaves.STATUS_NEXTWAVE then return "STATUS_NEXTWAVE" end if status == veafAirWaves.STATUS_OVER then return "STATUS_OVER" end return "" end veafAirWaves.STATUS_READY = 1 veafAirWaves.STATUS_WAITING_FOR_MORE_HUMANS = 1.5 veafAirWaves.STATUS_ACTIVE = 2 veafAirWaves.STATUS_WAITING_FOR_NEXTWAVE = 2.5 veafAirWaves.STATUS_NEXTWAVE = 3 veafAirWaves.STATUS_OVER = 4 veafAirWaves.MINIMUM_LIFE_FOR_AI_IN_PERCENT = 0 veafAirWaves.MAX_SECONDS_OUTSIDE_OF_ZONE_PLAYERS = nil -- no outside of zone mechanism by default for players veafAirWaves.MAX_SECONDS_OUTSIDE_OF_ZONE_IA = 30 veafAirWaves.DEFAULT_MESSAGE_START = "%s - online" veafAirWaves.DEFAULT_MESSAGE_WAIT_FOR_HUMANS = "%s - waiting %s seconds for more players" veafAirWaves.DEFAULT_MESSAGE_WAIT_TO_DEPLOY = "%s - waiting %s seconds before next wave" veafAirWaves.DEFAULT_MESSAGE_DEPLOY = "%s - deploying wave %s" veafAirWaves.DEFAULT_MESSAGE_DEPLOY_PLAYERS = "Wave %s deploying, %s" veafAirWaves.DEFAULT_MESSAGE_OUTSIDE_OF_ZONE_PLAYERS = "%s - you've been outside of the zone for %s seconds; go back inside, or you'll be destroyed after %s seconds." veafAirWaves.DEFAULT_MESSAGE_DESTROYED = "%s - wave %s has been destroyed" veafAirWaves.DEFAULT_MESSAGE_WON = "%s - won (no more waves)" veafAirWaves.DEFAULT_MESSAGE_LOST = "%s - lost (no more players)" veafAirWaves.DEFAULT_MESSAGE_STOP = "%s - offline" function AirWaveZone:new(objectToCopy) veaf.loggers.get(veafAirWaves.Id):debug("AirWave:new()") local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object AirWaveZone.init(objectToCreate) return objectToCreate end function AirWaveZone:setName(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[]:setName(%s)", veaf.p(value)) self.name = value return veafAirWaves.add(self) -- add the zone to the list as soon as a name is available to index it end function AirWaveZone:getName() return self.name or self.description end function AirWaveZone:setTriggerZone(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setTriggerZone(%s)", veaf.p(self.name), veaf.p(value)) self.triggerZoneName = value local triggerZone = veaf.getTriggerZone(value) if triggerZone then self:setZoneCenter({ x=triggerZone.x, y=triggerZone.y}) self:setZoneRadius(triggerZone.radius) else veaf.loggers.get(veafAirWaves.Id):error("AirWaveZone[%s]:setTriggerZone(): trigger zone [%s] does not exist", veaf.p(self.name), veaf.p(value)) end return self end function AirWaveZone:setZoneCenter(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setZoneCenter(%s)", veaf.p(self.name), veaf.p(value)) self.zoneCenter = value return self end function AirWaveZone:setZoneCenterFromCoordinates(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setZoneCenterFromCoordinates(%s)", veaf.p(self.name), veaf.p(value)) local _lat, _lon = veaf.computeLLFromString(value) local vec3 = coord.LLtoLO(_lat, _lon) return self:setZoneCenter(vec3) end function AirWaveZone:setZoneRadius(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setZoneRadius(%s)", veaf.p(self.name), veaf.p(value)) self.zoneRadius = value return self end function AirWaveZone:setDrawZone(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setDrawZone(%s)", veaf.p(self.name), veaf.p(value)) self.drawZone = value or false return self end function AirWaveZone:setDescription(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setDescription(%s)", veaf.p(self.name), veaf.p(value)) self.description = value return veafAirWaves.add(self) -- add the zone to the list as soon as a description is available to index it end function AirWaveZone:getDescription() return self.description or self.name end ---adds a wave of enemy planes ---parameters are very flexible: they can be: --- a table containing the following fields: --- - groups a list of groups or VEAF commands; VEAF commands can be prefixed with [lat, lon], specifying the location of their spawn relative to the center of the zone; default value is set with "setRespawnDefaultOffset" --- - number how many of these groups will actually be spawned (can be multiple times the same group!); it can be a "randomizable number", e.g., "2-6" for "between 2 and 6" --- - bias shifts the random generator to the right of the list; it can be a "randomizable number" too --- - delay the delay between this wave and the next one - if negative, then the next wave is spawned instantaneously (no waiting for this wave to be completed); it can be a "randomizable number" too --- or a list of strings (the groups or VEAF commands) --- or almost anything in between; we'll take a string as if it were a table containing one string, anywhere --- examples: --- :addWave("group1") --- :addWave("group1", "group2") --- :addWave({"group1", "group2"}) --- :addWave({ groups={"group1", "group2"}, number = 2}) --- :addWave({ groups="group1", number = 2}) ---returns self function AirWaveZone:addWave(...) veaf.loggers.get(veafAirWaves.Id):debug(string.format("AirWaveZone[%s]:addWave() : %s", veaf.p(self.name), veaf.p(arg))) ---@diagnostic disable-next-line: undefined-field this is a field defined in the vararg api local nArgs = arg.n or 0 if arg and nArgs > 0 then local groups = {} local number = 1 local bias = 0 local delay = nil for i = 1, nArgs, 1 do local parameter = arg[i] if type(parameter) == "string" then table.insert(groups, parameter) elseif type(parameter) == "table" then if parameter.groups then -- this is a parameters table, let's use it if type(parameter.groups) == "string" then -- we need a table groups = { parameter.groups } else groups = parameter.groups end number = parameter.number bias = parameter.bias delay = parameter.delay break else for j = 1, #parameter, 1 do local s = parameter[j] if type(s) == "string" then table.insert(groups, parameter) end end break end end end if not self.waves then self.waves = {} end table.insert(self.waves, {groups=groups, number=number or 1, bias=bias or 0, delay=delay}) end return self end ---reset the waves table to zero; useful when deep copying a zone to reset the waves and set something different ---@return table self function AirWaveZone:resetWaves() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:resetWaves()", veaf.p(self.name)) self.waves = {} return self end function AirWaveZone:setMessageStart(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMessageStart()", veaf.p(self.name)) self.messageStart = value return self end ---Set the onStart callback ---@param value function takes 2 parameters: the zone name (string), the monitored player units (table) ---@return table self function AirWaveZone:setOnStart(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setOnStart()", veaf.p(self.name)) self.onStart = value return self end function AirWaveZone:setMessageWaitForHumans(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMessageWaitForHumans()", veaf.p(self.name)) self.messageWaitForHumans = value return self end ---Set the onWaitForHumans callback ---@param value function takes 3 parameters: the zone name (string), the wave index (int), the monitored player units (table) ---@return table self function AirWaveZone:setOnWaitForHumans(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setOnWaitForHumans()", veaf.p(self.name)) self.onWaitForHumans = value return self end function AirWaveZone:setMessageWaitToDeploy(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMessageWaitToDeploy()", veaf.p(self.name)) self.messageWaitToDeploy = value return self end ---Set the onWaitToDeploy callback ---@param value function takes 3 parameters: the zone name (string), the wave index (int), the monitored player units (table) ---@return table self function AirWaveZone:setOnWaitToDeploy(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setOnWaitToDeploy()", veaf.p(self.name)) self.onWaitToDeploy = value return self end function AirWaveZone:setMessageDeploy(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMessageDeploy()", veaf.p(self.name)) self.messageDeploy = value return self end function AirWaveZone:setMessageDeployPlayers(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMessageDeployPlayers()", veaf.p(self.name)) self.messageDeployPlayers = value return self end ---Set the onDeploy callback ---@param value function takes 3 parameters: the zone name (string), the wave index (int), the monitored player units (table) ---@return table self function AirWaveZone:setOnDeploy(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setOnDeploy()", veaf.p(self.name)) self.onDeploy = value return self end function AirWaveZone:setMessageDestroyed(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMessageDestroyed()", veaf.p(self.name)) self.messageDestroyed = value return self end ---Set the onDestroyed callback ---@param value function takes 3 parameters: the zone name (string), the wave index (int), the monitored player units (table) ---@return table self function AirWaveZone:setOnDestroyed(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setOnDestroyed()", veaf.p(self.name)) self.onDestroyed = value return self end function AirWaveZone:setMessageOutsideOfZone(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMessageOutsideOfZone()", veaf.p(self.name)) self.messageOutsideOfZone = value return self end ---Set the onOutsideOfZone callback ---@param value function takes 3 parameters: the zone name (string), the wave index (int), the monitored player units (table) ---@return table self function AirWaveZone:setOnOutsideOfZone(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setOnOutsideOfZone()", veaf.p(self.name)) self.onOutsideOfZone = value return self end function AirWaveZone:setMessageWon(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMessageWon()", veaf.p(self.name)) self.messageWon = value return self end ---Set the onWon callback ---@param value function takes 2 parameters: the zone name (string), the monitored player units (table) ---@return table self function AirWaveZone:setOnWon(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setOnWon()", veaf.p(self.name)) self.onWon = value return self end function AirWaveZone:setMessageLost(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMessageLost()", veaf.p(self.name)) self.messageLost = value return self end ---Set the onLost callback ---@param value function takes 2 parameters: the zone name (string), the monitored player units (table) ---@return table self function AirWaveZone:setOnLost(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setOnLost()", veaf.p(self.name)) self.onLost = value return self end function AirWaveZone:setMessageStop(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMessageStop()", veaf.p(self.name)) self.messageStop = value return self end ---Set the onStop callback ---@param value function takes 2 parameters: the zone name (string), the monitored player units (table) ---@return table self function AirWaveZone:setOnStop(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setOnStop()", veaf.p(self.name)) self.onStop = value return self end function AirWaveZone:setSilent(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setSilent(%s)", veaf.p(self.name), veaf.p(value)) self.silent = value or false return self end function AirWaveZone:setRespawnRadius(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setRespawnRadius(%s)", veaf.p(self.name), veaf.p(value)) self.respawnRadius = value if self.respawnRadius < 250 then self.respawnRadius = 250 end return self end ---set the default respawn offset (in meters, relative to the zone center) ---@param defaultOffsetLatitude any in meters ---@param defaultOffsetLongitude any in meters ---@return table self function AirWaveZone:setRespawnDefaultOffset(defaultOffsetLatitude, defaultOffsetLongitude) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setRespawnDefaultOffset(%s, %s)", veaf.p(self.name), veaf.p(defaultOffsetLatitude), veaf.p(defaultOffsetLongitude)) self.respawnDefaultOffset = { latDelta = defaultOffsetLatitude, lonDelta = defaultOffsetLongitude} return self end function AirWaveZone:addPlayerCoalition(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:addPlayerCoalition(%s)", veaf.p(self.name), veaf.p(value)) self.playerCoalitions[value] = value return self end function AirWaveZone:getPlayerCoalition() local result = nil for coalition, _ in pairs(self.playerCoalitions) do result = coalition break end return result end ---Sets the default delay in seconds between waves of enemy planes ---@param value number a delay in seconds ---@return table self function AirWaveZone:setDelayBetweenWaves(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setDelayBetweenWaves(%s)", veaf.p(self.name), veaf.p(value)) self.delayBetweenWaves = value return self end ---Sets the delay in seconds between the first human in zone and the actual activation of the zone ---@param value number a delay in seconds ---@return table self function AirWaveZone:setDelayBeforeActivation(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setDelayBeforeActivation(%s)", veaf.p(self.name), veaf.p(value)) self.delayBeforeActivation = value return self end function AirWaveZone:setResetWhenDying(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setResetWhenDying(%s)", veaf.p(self.name), veaf.p(value)) self.resetWhenDying = value return self end function AirWaveZone:setMinimumAltitudeInFeet(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMinimumAltitudeInFeet(%s)", veaf.p(self.name), veaf.p(value)) self.minimumAltitude = value * 0.3048 -- convert from feet return self end function AirWaveZone:getMinimumAltitudeInMeters() return self.minimumAltitude end function AirWaveZone:setMaximumAltitudeInFeet(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMaximumAltitudeInFeet(%s)", veaf.p(self.name), veaf.p(value)) self.maximumAltitude = value * 0.3048 -- convert from feet return self end function AirWaveZone:getMaximumAltitudeInMeters() return self.maximumAltitude end ---Sets the maximum number of seconds an IA can stay out of its zone before being destroyed ---@param value number a delay in seconds ---@return table self function AirWaveZone:setMaxSecondsOutsideOfZoneIA(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMaxSecondsOutsideOfZoneIA(%s)", veaf.p(self.name), veaf.p(value)) self.maxSecondsOutsideOfZoneIA = value return self end ---Disables the check for IA out of zone. ---@return table self function AirWaveZone:disableOutsideOfZoneIA() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:disableOutsideOfZoneIA()", veaf.p(self.name)) self.maxSecondsOutsideOfZoneIA = nil return self end ---Sets the maximum number of seconds a player can stay out of its zone before being destroyed; players will be messaged as soon as they exit the zone, and every check ---@param value number a delay in seconds ---@return table self function AirWaveZone:setMaxSecondsOutsideOfZonePlayers(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMaxSecondsOutsideOfZonePlayers(%s)", veaf.p(self.name), veaf.p(value)) self.maxSecondsOutsideOfZonePlayers = value return self end ---Disables the check for players out of zone. ---@return table self function AirWaveZone:disableOutsideOfZonePlayers() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:disableOutsideOfZonePlayers()", veaf.p(self.name)) self.maxSecondsOutsideOfZonePlayers = nil return self end function AirWaveZone:_setState(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:_setState(%s)", veaf.p(self.name), veaf.p(veafAirWaves.statusToString(value))) self.state = value return self end ---the function that decides if a wave is dead or not (as a set of groups and units) ---@param callback function the callback function will be called with 3 parameters: a zone, the wave index number, the spawned groups names list; it must return a boolean function AirWaveZone:setIsEnemyWaveDeadCallback(callback) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setIsEnemyWaveDeadCallback()", veaf.p(self.name)) self.isEnemyWaveDeadCallback = callback return self end ---the function that decides if a group is dead or not (individually) ---@param callback function the callback function will be called with 3 parameters: a zone, the wave index number, a DCS group table; it must return a boolean function AirWaveZone:setIsEnemyGroupDeadCallback(callback) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setIsEnemyGroupDeadCallback()", veaf.p(self.name)) self.isEnemyGroupDeadCallback = callback return self end ---Sets the minimum percentage of life that an AI unit is supposed to have to be considered alive ---@param value number percentage ---@return table function AirWaveZone:setMinimumLifeForAiInPercent(value) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setMinimumLifeForAiInPercent(%s)", veaf.p(self.name), veaf.p(value)) self.minimumLifeForAiInPercent = value return self end --- the function that handles crippled enemy units ---@param callback function the callback function will be called with 3 parameters: a zone, the wave index number, a DCS unit table; it must do what it wants with the unit function AirWaveZone:setHandleCrippledEnemyUnitCallback(callback) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:setHandleCrippledEnemyUnitCallback()", veaf.p(self.name)) self.handleCrippledEnemyUnitCallback = callback return self end function AirWaveZone:reset() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:reset()", veaf.p(self.name)) -- despawn the ennemies self:destroyCurrentWave() -- reset all the zone properties -- player units (if they die, reset the zone) self.playerUnitsNames = {} -- groups that have been spawned (the current wave) self.spawnedGroupsNames = {} -- the delay after this wave, and before the next one (either set in the wave definition, or it's the default delayBetweenWaves) self.delayBeforeNextWave = nil -- the time when the next wave is supposed to spawn (used to know when to actually spawn when in the STATUS_WAITING_FOR_NEXTWAVE state) self.timeOfNextWave = nil -- the time when the zones is supposed to be activated (used to know when to actually activate when in the STATUS_WAITING_FOR_MORE_HUMANS state) self.timeOfActivation = nil -- human units that are being watched self.playerHumanUnits = nil -- names of the human units that are being watched self.playerHumanUnitsNames = nil -- IA units that are being watched self.unitsInZone = {} -- current wave number self.currentWaveIndex = 0 -- the drawing object that has been used to draw the zone self.zoneDrawing = nil -- the time humans exited the zone self.timestampsOutOfZone = {} -- deschedule the check() function if self.checkFunctionSchedule then mist.removeFunction(self.checkFunctionSchedule) self.checkFunctionSchedule = nil end return self end -- the function that decides if a wave is dead or not (as a set of groups and units) function AirWaveZone:isEnemyWaveDead(waveNumber, waveGroupsNames) --veaf.loggers.get(veafAirWaves.Id):trace("AirWaveZone[%s]:isEnemyWaveDead(%s)", veaf.p(self.name), veaf.p(waveNumber)) --veaf.loggers.get(veafAirWaves.Id):trace("waveGroupsNames=%s", veaf.p(waveGroupsNames)) local currentWaveAlive = false for _, groupName in pairs(waveGroupsNames) do local group = Group.getByName(groupName) if group then local groupIsDead = self.isEnemyGroupDeadCallback(self, self.currentWaveIndex, group) if not groupIsDead then currentWaveAlive = true end end end return not currentWaveAlive end -- the function that decides if IA ennemy groups are dead (individually) function AirWaveZone:isEnemyGroupDead(waveNumber, group) --veaf.loggers.get(veafAirWaves.Id):trace("AirWaveZone[%s]:isEnemyGroupDead(%s)", veaf.p(self.name), veaf.p(waveNumber)) if not group then return true end --veaf.loggers.get(veafAirWaves.Id):trace("group:getName()=%s", veaf.p(group:getName())) local groupAtLeastOneUnitAlive = false local category = group:getCategory() local units = group:getUnits() if units then for _,unit in pairs(units) do local unitAlive = false local unitLife = unit:getLife() local unitLife0 = 0 if unit.getLife0 then -- statics have no life0 unitLife0 = unit:getLife0() end local unitLifePercent = unitLife if unitLife0 > 0 then unitLifePercent = 100 * unitLife / unitLife0 end if unitLifePercent > self.minimumLifeForAiInPercent then if category == 0 --[[airplanes]] or category == 1 --[[helicopters]] then if unit:inAir() then unitAlive = true end else unitAlive = true end end if not unitAlive then self.handleCrippledEnemyUnitCallback(self, self.currentWaveIndex, unit) else groupAtLeastOneUnitAlive = true end end end return not groupAtLeastOneUnitAlive end -- the function that handles crippled enemy units function AirWaveZone:handleCrippledEnemyUnit(waveNumber, unit) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:handleCrippledEnemyUnit(%s)", veaf.p(self.name), veaf.p(waveNumber)) if not unit then return end veaf.loggers.get(veafAirWaves.Id):debug("unit:getName()=%s", veaf.p(unit:getName())) -- simply despawn the unit unit:destroy() end function AirWaveZone:getPlayerUnitsNames() if not self.playerHumanUnitsNames then self.playerHumanUnitsNames = {} for _, unit in pairs(mist.DBs.humansByName) do local coalitionId = 0 if unit.coalition then if unit.coalition:lower() == "red" then coalitionId = coalition.side.RED elseif unit.coalition:lower() == "blue" then coalitionId = coalition.side.BLUE end end if self.playerCoalitions[coalitionId] then if unit.category then if unit.category == "plane" then table.insert(self.playerHumanUnitsNames, unit.unitName) end end end end end return self.playerHumanUnitsNames end function AirWaveZone:check() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:check() -> self.state=%s", veaf.p(self.name), veaf.p(veafAirWaves.statusToString(self.state))) veaf.loggers.get(veafAirWaves.Id):trace("AirWaveZone[%s]:check() -> timer.getTime()=%s", veaf.p(self.name), veaf.p(timer.getTime())) local function getHumansInZone() local resultUnitsByName = {} local resultUnitsNames = {} local resultUnits = {} local unitNames = self:getPlayerUnitsNames() local triggerZone = veaf.getTriggerZone(self.triggerZoneName) local humanUnits = nil if triggerZone then if triggerZone.type == 0 then -- circular humanUnits = mist.getUnitsInZones(unitNames, {self.triggerZoneName}) elseif triggerZone.type == 2 then -- quad point humanUnits = mist.getUnitsInPolygon(unitNames, triggerZone.verticies) end elseif self.zoneCenter then humanUnits = veaf.findUnitsInCircle(self.zoneCenter, self.zoneRadius, false, unitNames) else veaf.loggers.get(veafAirWaves.Id):error("No triggerzone, and no zone center/radius defined!") end for _, unit in pairs(humanUnits) do -- check the unit altitude against the ceiling and floor if unit:inAir() then -- never count a landed aircraft local alt = unit:getPoint().y if alt >= self:getMinimumAltitudeInMeters() and alt <= self:getMaximumAltitudeInMeters() then -- add the unit to the player units list, so that we can monitor it local unitName = unit:getName() table.insert(resultUnitsNames, unitName) table.insert(resultUnits, unit) resultUnitsByName[unitName] = unit end end end return resultUnits, resultUnitsNames, resultUnitsByName end local humansInZone, humansInZoneNames, humansInZoneByName = getHumansInZone() -- whatever the state, monitor the player units if they're defined if self.playerUnitsNames and #self.playerUnitsNames > 0 then local atLeastOnePlayerAlive = false local atLeastOnePlayerAirborne = false for _, unitName in pairs(self.playerUnitsNames) do local unit = Unit.getByName(unitName) if unit then -- check alive atLeastOnePlayerAlive = true if unit:inAir() then atLeastOnePlayerAirborne = true end -- check in zone if humansInZoneByName[unitName] then self.timestampsOutOfZone[unitName] = nil elseif self.maxSecondsOutsideOfZonePlayers then local timestampOutOfZone = timer.getTime() if self.timestampsOutOfZone[unitName] then timestampOutOfZone = self.timestampsOutOfZone[unitName] else self.timestampsOutOfZone[unitName] = timestampOutOfZone end local seconds = timer.getTime() - timestampOutOfZone self:signalOutsideOfZone(unitName, seconds) local secondsOffend = seconds - self.maxSecondsOutsideOfZonePlayers if secondsOffend > 0 then -- destroy the player if secondsOffend > self.maxSecondsOutsideOfZonePlayers then veaf.loggers.get(veafAirWaves.Id):debug("destroy out of zone player unitName=%s", veaf.p(unitName)) unit:destroy() else veaf.loggers.get(veafAirWaves.Id):debug("flak out of zone player unitName=%s", veaf.p(unitName)) local point = unit:getPoint() local positionForFlak1 = mist.vec.add(point, mist.vec.scalarMult(unit:getVelocity(), 1)) local positionForFlak2 = mist.vec.add(point, mist.vec.scalarMult(unit:getVelocity(), 2)) local positionForFlak3 = mist.vec.add(point, mist.vec.scalarMult(unit:getVelocity(), 3)) veafSpawn.spawnBomb(positionForFlak1, 50, 5, 25 + seconds - self.maxSecondsOutsideOfZonePlayers, positionForFlak1.y, 50) veafSpawn.spawnBomb(positionForFlak2, 50, 5, 25 + seconds - self.maxSecondsOutsideOfZonePlayers, positionForFlak2.y, 50) veafSpawn.spawnBomb(positionForFlak3, 50, 5, 25 + seconds - self.maxSecondsOutsideOfZonePlayers, positionForFlak3.y, 50) end end end end end if not (atLeastOnePlayerAlive and atLeastOnePlayerAirborne) then veaf.loggers.get(veafAirWaves.Id):debug("player is dead or despawned in %s", veaf.p(self:getName())) if self.state ~= veafAirWaves.STATUS_OVER then -- signal that all players have been destroyed self:signalLost() end if self.resetWhenDying then -- reset the zone self:stop() self:start() end end end if self.state == veafAirWaves.STATUS_READY then if humansInZone and #humansInZone > 0 then -- store the human units that we're going to monitor self.unitsInZone = humansInZone self.playerUnitsNames = humansInZoneNames self:_setState(veafAirWaves.STATUS_WAITING_FOR_MORE_HUMANS) if self.delayBeforeActivation and self.delayBeforeActivation > 0 then self:signalWaitForHumans() end self.timeOfActivation = timer.getTime() + self.delayBeforeActivation veaf.loggers.get(veafAirWaves.Id):debug("waiting %s seconds before activation", veaf.p(self.delayBeforeActivation)) veaf.loggers.get(veafAirWaves.Id):trace("self.timeOfActivation=%s", veaf.p(self.timeOfActivation)) veaf.loggers.get(veafAirWaves.Id):debug("restart the check immediately") -- restart the check immediately (we don't want to wait for the next state to be processed) self:check() end elseif self.state == veafAirWaves.STATUS_WAITING_FOR_MORE_HUMANS then -- wait until the delay has passed if self.timeOfActivation and timer.getTime() >= self.timeOfActivation then -- zone is ready, check for players entering local humansInZone, humanInZoneNames = getHumansInZone() if humansInZone and #humansInZone > 0 then -- store the human units that we're going to monitor self.unitsInZone = humansInZone self.playerUnitsNames = humanInZoneNames -- reset wave index self.currentWaveIndex = 0 self:_setState(veafAirWaves.STATUS_NEXTWAVE) -- restart the check immediately (we don't want to wait for the next state to be processed) self:check() end end elseif self.state == veafAirWaves.STATUS_NEXTWAVE then -- wave has been destroyed, or it's the first time a wave has to be deployed; check if there is a next one and deploy it if self.currentWaveIndex < #self.waves then if not self.delayBeforeNextWave then self.delayBeforeNextWave = self.delayBetweenWaves end self:_setState(veafAirWaves.STATUS_WAITING_FOR_NEXTWAVE) if self.delayBeforeNextWave and self.delayBeforeNextWave > 0 then self:signalWaitToDeploy() end self.timeOfNextWave = timer.getTime() + self.delayBeforeNextWave veaf.loggers.get(veafAirWaves.Id):debug("waiting %s seconds before spawning next wave(s)", veaf.p(self.delayBeforeNextWave)) veaf.loggers.get(veafAirWaves.Id):trace("self.timeOfNextWave=%s", veaf.p(self.timeOfNextWave)) -- restart the check immediately (we don't want to wait for the next state to be processed) self:check() else self:signalWon() self:_setState(veafAirWaves.STATUS_OVER) end elseif self.state == veafAirWaves.STATUS_WAITING_FOR_NEXTWAVE then -- wait until the delay has passed if self.timeOfNextWave and timer.getTime() >= self.timeOfNextWave then -- deploy the next wave local spawnedGroups, delayBeforeNextWave = self:deployWaves() if spawnedGroups then self.delayBeforeNextWave = delayBeforeNextWave or self.delayBetweenWaves self:_setState(veafAirWaves.STATUS_ACTIVE) end end elseif self.state == veafAirWaves.STATUS_ACTIVE then -- zone is active -- check if the current wave is still alive local waveIsDead = self.isEnemyWaveDeadCallback(self, self.currentWaveIndex, self.spawnedGroupsNames) if waveIsDead then -- clean up any eventual remaining group of the wave self:destroyCurrentWave() -- signal that wave has been destroyed self:signalDestroyed() -- prepare next wave self:_setState(veafAirWaves.STATUS_NEXTWAVE) -- restart the check immediately (we don't want to wait for the next state to be processed) self:check() else -- check if any IA wandered out of the zone for longer than it should have (maxSecondsOutsideOfZoneIA) local triggerZone = veaf.getTriggerZone(self.triggerZoneName) for _, groupName in pairs(self.spawnedGroupsNames) do local group = Group.getByName(groupName) if group then local units = group:getUnits() if units then for _, unit in pairs(units) do local unitName = unit:getName() local outOfZone = false if self.maxSecondsOutsideOfZoneIA then -- no need to check if feature is disabled if triggerZone then outOfZone = not(veaf.isUnitInZone(unit, triggerZone)) else local pos = unit:getPosition().p if pos then -- you never know O.o local distanceFromCenter = ((pos.x - self.zoneCenter.x)^2 + (pos.z - self.zoneCenter.z)^2)^0.5 outOfZone = (distanceFromCenter > self.zoneRadius) end end if outOfZone then local timestampOutOfZone = timer.getTime() if self.timestampsOutOfZone[unitName] then timestampOutOfZone = self.timestampsOutOfZone[unitName] else self.timestampsOutOfZone[unitName] = timestampOutOfZone end local seconds = timer.getTime() - timestampOutOfZone local secondsOffend = seconds - self.maxSecondsOutsideOfZoneIA if secondsOffend > 0 then -- destroy the IA veaf.loggers.get(veafAirWaves.Id):debug("destroy out of zone AI unitName=%s", veaf.p(unitName)) unit:destroy() end else self.timestampsOutOfZone[unitName] = nil end end end end end end end elseif self.state == veafAirWaves.STATUS_OVER then -- zone has still to be reset to restart end if self.checkFunctionSchedule then -- deschedule if needed mist.removeFunction(self.checkFunctionSchedule) self.checkFunctionSchedule = nil end self.checkFunctionSchedule = mist.scheduleFunction(AirWaveZone.check, {self}, timer.getTime() + veafAirWaves.WATCHDOG_DELAY + math.random(0, 2)) -- randomize reschedules so not all zones are working at the same time end function AirWaveZone:chooseGroupsToDeploy() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:chooseGroupsToDeploy()", veaf.p(self.name)) if self.currentWaveIndex <= #self.waves then local nextWave = self.waves[self.currentWaveIndex] if nextWave then -- process a random group definition local groupsToChooseFrom = nextWave.groups local numberOfGroups = nextWave.number local bias = nextWave.bias local delay = nextWave.delay local result = {} if type(numberOfGroups) == "string" then -- convert randomizable numeric to number numberOfGroups = veaf.getRandomizableNumeric(numberOfGroups) end if type(bias) == "string" then -- convert randomizable numeric to number bias = veaf.getRandomizableNumeric(bias) end if delay ~= nil and type(delay) == "string" then -- convert randomizable numeric to number delay = veaf.getRandomizableNumeric(delay) end if groupsToChooseFrom and type(groupsToChooseFrom) == "table" and numberOfGroups and type(numberOfGroups) == "number" and bias and type(bias) == "number" then for _ = 1, numberOfGroups do local group = veaf.randomlyChooseFrom(groupsToChooseFrom, bias) table.insert(result, group) end end return result, delay end end end function AirWaveZone:deployWaves() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:deployWaves()", veaf.p(self.name)) self.spawnedGroupsNames = {} local groupsToDeployForTheseWaves = {} local lastDelay repeat self.currentWaveIndex = self.currentWaveIndex + 1 local groupsToDeploy, delay = self:chooseGroupsToDeploy() veaf.loggers.get(veafAirWaves.Id):debug("groupsToDeploy=%s", veaf.p(groupsToDeploy)) veaf.loggers.get(veafAirWaves.Id):debug("delay=%s", veaf.p(delay)) lastDelay = delay for _, group in pairs(groupsToDeploy) do table.insert(groupsToDeployForTheseWaves, group) end until not lastDelay or lastDelay >= 0 or self.currentWaveIndex >= #self.waves if groupsToDeployForTheseWaves then local zoneCenter = {} if self.triggerZoneName then local triggerZone = veaf.getTriggerZone(self.triggerZoneName) zoneCenter.x = triggerZone.x zoneCenter.z = triggerZone.y zoneCenter.y = 0 elseif self.zoneCenter then zoneCenter = self.zoneCenter end for _, groupNameOrCommand in pairs(groupsToDeployForTheseWaves) do -- check if this is a DCS group or a VEAF command if veaf.startsWith(groupNameOrCommand, "[") or veaf.startsWith(groupNameOrCommand, "-") then -- this is a command local command = groupNameOrCommand local latDelta = self.respawnDefaultOffset.latDelta local lonDelta = self.respawnDefaultOffset.lonDelta if veaf.startsWith(groupNameOrCommand, "[") then -- extract relative coordinates and the actual command local coords coords, command = groupNameOrCommand:match("%[(.*)%](.*)") if coords then latDelta, lonDelta = coords:match("([%+-%d]+),%s*([%+-%d]+)") end end veaf.loggers.get(veafAirWaves.Id):debug("running command [%s]", veaf.p(command)) local position = {x = zoneCenter.x - lonDelta, y = zoneCenter.y, z = zoneCenter.z + latDelta} local randomPosition = mist.getRandPointInCircle(position, self.respawnRadius) local spawnedGroupsNames = {} veafInterpreter.execute(command, randomPosition, self.coalition, nil, spawnedGroupsNames) for _, newGroupName in pairs(spawnedGroupsNames) do table.insert(self.spawnedGroupsNames, newGroupName) end else -- this is a DCS group local groupName = groupNameOrCommand veaf.loggers.get(veafAirWaves.Id):debug("spawning group [%s]", veaf.p(groupName)) local groupData = mist.getGroupData(groupName) veaf.loggers.get(veafAirWaves.Id):trace("groupData=%s", veaf.p(groupData)) if not groupData then veaf.loggers.get(veafAirWaves.Id):error("group [%s] does not exist in the mission!", veaf.p(groupName)) else local spawnSpot = {x = zoneCenter.x - self.respawnDefaultOffset.lonDelta, y = zoneCenter.y, z = zoneCenter.z + self.respawnDefaultOffset.latDelta} -- Try and set the spawn spot at the place the group has been set in the Mission Editor. -- Unfortunately this is sometimes not possible because DCS is not returning the group units for some reason. -- When this happens we'll default to the default spawn offset (same as spawning with VEAF commands) if not groupData.units[1] then veaf.loggers.get(veafAirWaves.Id):warn("group [%s] does not have any unit!", veaf.p(groupName)) else spawnSpot = { x = groupData.units[1].x, y = groupData.units[1].alt, z = groupData.units[1].y } end veaf.loggers.get(veafAirWaves.Id):trace("spawnSpot=%s", veaf.p(spawnSpot)) local vars = {} vars.point = mist.getRandPointInCircle(spawnSpot, self.respawnRadius) vars.point.z = vars.point.y vars.point.y = spawnSpot.y vars.gpName = groupName vars.action = 'clone' vars.route = mist.getGroupRoute(groupName, 'task') veaf.loggers.get(veafAirWaves.Id):trace("vars=%s", veaf.p(vars)) local newGroup = mist.teleportToPoint(vars) -- respawn with radius if newGroup then table.insert(self.spawnedGroupsNames, newGroup.name) end end end end veaf.loggers.get(veafAirWaves.Id):trace("self.spawnedGroupsNames=%s", veaf.p(self.spawnedGroupsNames)) self:_setState(veafAirWaves.STATUS_ACTIVE) end self:signalDeploy() return (self.spawnedGroupsNames and #self.spawnedGroupsNames > 0), lastDelay end ---Sends a message to all players in the zone, but only once per group (because we're actually messaging whole groups, thanks DCS) ---@param msg string the message to be sent function AirWaveZone:signalToPlayers(msg) if self.unitsInZone then for _, unitInZone in pairs(self.unitsInZone) do local unitName = unitInZone:getName() veaf.outTextForUnit(unitName, msg, 15) end end end function AirWaveZone:signalStart() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:signalStart()", veaf.p(self.name)) if not self.silent then local msg = string.format(self.messageStart, self:getDescription()) for coalition, _ in pairs(self.playerCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onStart then self.onStart(self.name, self.playerUnitsNames) end end function AirWaveZone:signalWaitForHumans() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:signalWaitForHumans()", veaf.p(self.name)) if not self.silent then self:signalToPlayers(string.format(self.messageWaitForHumans, self:getDescription(), self.delayBeforeActivation)) end if self.onWaitForHumans then self.onWaitForHumans(self.name, self.playerUnitsNames) end end function AirWaveZone:signalWaitToDeploy() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:signalWaitToDeploy()", veaf.p(self.name)) if not self.silent then self:signalToPlayers(string.format(self.messageWaitToDeploy, self:getDescription(), self.delayBeforeNextWave)) end if self.onWaitToDeploy then self.onWaitToDeploy(self.name, self.playerUnitsNames) end end function AirWaveZone:signalDeploy() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:signalDeploy()", veaf.p(self.name)) if not self.silent then -- messages to all local msg = string.format(self.messageDeploy, self:getDescription(), self.currentWaveIndex) for coalition, _ in pairs(self.playerCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end -- messages to players with BRAA if self.unitsInZone then local groupsAlreadyMessaged = {} for _, unitInZone in pairs(self.unitsInZone) do -- compute BRAA of closest group local braa = { bearing = -1, distance = 9999} for _, spawnedGroupName in pairs(self.spawnedGroupsNames) do local spawnedGroupPosition = mist.getAvgGroupPos(spawnedGroupName) local unitPosition = nil if unitInZone and unitInZone:getPosition() then unitPosition = unitInZone:getPosition().p end if spawnedGroupPosition and unitPosition then local bearing, _, _, distanceInNm = veaf.getBearingAndRangeFromTo(unitPosition, spawnedGroupPosition) if braa.distance > distanceInNm then -- this is closer than the group we had before braa.distance = math.floor(distanceInNm) braa.bearing = math.floor(bearing) end end end if braa.bearing > -1 then -- found a group local braaS = string.format("BRA %03d/%02d", braa.bearing, braa.distance) if braa.distance < 5 then braaS = "MERGED" end local group = unitInZone:getGroup() local groupId = nil if group then groupId = group:getID() end if groupId and not groupsAlreadyMessaged[groupId] then groupsAlreadyMessaged[groupId] = true trigger.action.outTextForGroup(groupId, string.format(self.messageDeployPlayers, self.currentWaveIndex, braaS), 15) end end end end end if self.onDeploy then self.onDeploy(self.name, self.currentWaveIndex, self.playerUnitsNames) end end function AirWaveZone:signalDestroyed() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:signalDestroyed()", veaf.p(self.name)) if not self.silent then self:signalToPlayers(string.format(self.messageDestroyed, self:getDescription(), self.currentWaveIndex)) end if self.onDestroyed then self.onDestroyed(self.name, self.currentWaveIndex, self.playerUnitsNames) end end function AirWaveZone:signalOutsideOfZone(playerUnitName, seconds) veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:signalOutsideOfZone(player=%s, seconds=%s)", veaf.p(self.name), veaf.p(playerUnitName), veaf.p(seconds)) if not self.silent then veaf.outTextForUnit(playerUnitName, string.format(self.messageOutsideOfZone, self:getDescription(), seconds, self.maxSecondsOutsideOfZonePlayers), 15) end if self.onOutsideOfZone then self.onOutsideOfZone(self.name, playerUnitName, seconds) end end function AirWaveZone:signalWon() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:signalWon()", veaf.p(self.name)) if not self.silent then self:signalToPlayers(string.format(self.messageWon, self:getDescription())) end if self.onWon then self.onWon(self.name, self.playerUnitsNames) end end function AirWaveZone:signalLost() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:signalLost()", veaf.p(self.name)) if not self.silent then local msg = string.format(self.messageLost, self:getDescription()) for coalition, _ in pairs(self.playerCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onLost then self.onLost(self.name, self.playerUnitsNames) end end function AirWaveZone:signalStop() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:signalStop()", veaf.p(self.name)) if not self.silent then local msg = string.format(self.messageStop, self:getDescription()) for coalition, _ in pairs(self.playerCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onStop then self.onStop(self.name, self.playerUnitsNames) end end function AirWaveZone:start() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:start()", veaf.p(self.name)) self:reset() self:_setState(veafAirWaves.STATUS_READY) self:check() -- draw the zone if self.drawZone then if self.triggerZoneName then self.zoneDrawing = mist.marker.drawZone(self.triggerZoneName, {message=self:getDescription(), readOnly=true}) else self.zoneDrawing = VeafCircleOnMap:new() :setName(self:getName()) :setCoalition(self:getPlayerCoalition()) :setCenter(self.zoneCenter) :setRadius(self.zoneRadius) :setLineType("dashed") :setColor("white") :setFillColor("transparent") :draw() end end self:signalStart() return self end function AirWaveZone:stop() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:stop()", veaf.p(self.name)) self:reset() self:_setState(veafAirWaves.STATUS_STOP) -- erase the zone if self.zoneDrawing then if self.triggerZoneName then mist.marker.remove(self.zoneDrawing.markId) else self.zoneDrawing:erase() end self.zoneDrawing = nil end self:signalStop() return self end function AirWaveZone:destroyCurrentWave() veaf.loggers.get(veafAirWaves.Id):debug("AirWaveZone[%s]:destroyCurrentWave()", veaf.p(self.name)) if self.spawnedGroupsNames then for _, _groupName in pairs(self.spawnedGroupsNames) do local _group = Group.getByName(_groupName) if _group then _group:destroy() end end end self.spawnedGroupsNames = {} return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafAirWaves.add(aWaveZone, aName) local name = aName or aWaveZone:getName() veafAirWaves.zones[name] = aWaveZone return aWaveZone end function veafAirWaves.get(aNameString) return veafAirWaves.zones[aNameString] end veaf.loggers.get(veafAirWaves.Id):info(string.format("Loading version %s", veafAirWaves.Version)) ------------------ END script veafAirWaves.lua ------------------ ------------------ START script veafAssets.lua ------------------ ------------------------------------------------------------------ -- VEAF assets (important groups in a mission) management functions for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Manages the assets that exist the map (tankers, awacs, ...) -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafAssets = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafAssets.Id = "ASSETS" --- Version. veafAssets.Version = "1.8.2" -- trace level, specific to this module --veafAssets.LogLevel = "trace" veaf.loggers.new(veafAssets.Id, veafAssets.LogLevel) veafAssets.Assets = { -- list the assets common to all missions below } veafAssets.RadioMenuName = "ASSETS" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafAssets.rootPath = nil veafAssets.assets = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafAssets._buildAssetRadioMenu(menu, title, element) if element.disposable or element.information then -- in this case we need a submenu local radioMenu = veafRadio.addSubMenu(element.description, menu) veafRadio.addCommandToSubmenu("Respawn "..element.description, radioMenu, veafAssets.respawn, element.name, veafRadio.USAGE_ForAll) if element.information then veafRadio.addCommandToSubmenu("Get info on "..element.description, radioMenu, veafAssets.info, element.name, veafRadio.USAGE_ForGroup) end if element.disposable then veafRadio.addSecuredCommandToSubmenu("Dispose of "..element.description, radioMenu, veafAssets.dispose, element.name, veafRadio.USAGE_ForAll) end else veafRadio.addCommandToSubmenu("Respawn "..element.description, menu, veafAssets.respawn, element.name, veafRadio.USAGE_ForAll) end end --- Build the initial radio menu function veafAssets.buildRadioMenu() -- don't create an empty menu if veaf.length(veafAssets.assets) == 0 then return end veafAssets.rootPath = veafRadio.addSubMenu(veafAssets.RadioMenuName) if not(veafRadio.skipHelpMenus) then veafRadio.addCommandToSubmenu("HELP", veafAssets.rootPath, veafAssets.help, nil, veafRadio.USAGE_ForGroup) end veafRadio.addPaginatedRadioElements(veafAssets.rootPath, veafAssets._buildAssetRadioMenu, veafAssets.assets, "description", "sort") veafRadio.refreshRadioMenu() end function veafAssets.info(parameters) local name, unitName = veaf.safeUnpack(parameters) veaf.loggers.get(veafAssets.Id):debug("veafAssets.info "..name) local theAsset = nil for _, asset in pairs(veafAssets.assets) do if asset.name == name then theAsset = asset end end if theAsset then local group = Group.getByName(theAsset.name) veaf.loggers.get(veafAssets.Id):trace(string.format("assets[%s] = '%s'",theAsset.name, theAsset.description)) local text = theAsset.description .. " is not active nor alive" if group then veaf.loggers.get(veafAssets.Id):debug("found asset group") local nAlive = 0 for _, unit in pairs(group:getUnits()) do veaf.loggers.get(veafAssets.Id):trace("unit life = "..unit:getLife()) if unit:getLife() >= 1 then nAlive = nAlive + 1 end end if nAlive > 0 then if nAlive == 1 then text = string.format("%s is active ; one unit is alive\n", theAsset.description) else text = string.format("%s is active ; %d units are alive\n", theAsset.description, nAlive) end if theAsset.information then text = text .. theAsset.information end end end veaf.outTextForGroup(unitName, text, 30) end end function veafAssets.dispose(name) veaf.loggers.get(veafAssets.Id):debug("veafAssets.dispose "..name) local theAsset = nil for _, asset in pairs(veafAssets.assets) do if asset.name == name then theAsset = asset end end if theAsset then veaf.loggers.get(veafAssets.Id):debug("veafSpawn.destroy "..theAsset.name) local group = Group.getByName(theAsset.name) if group then for _, unit in pairs(group:getUnits()) do Unit.destroy(unit) end end local text = "I've disposed of " .. theAsset.description trigger.action.outText(text, 30) end end function veafAssets.respawn(name) veaf.loggers.get(veafAssets.Id):debug("veafAssets.respawn "..name) local theAsset = nil for _, asset in pairs(veafAssets.assets) do if asset.name == name then theAsset = asset end end if theAsset then mist.respawnGroup(name, true) if theAsset.linked then veaf.loggers.get(veafAssets.Id):trace(string.format("veafAssets[%s].linked=%s",name, veaf.p(theAsset.linked))) -- there are linked groups to respawn if type(theAsset.linked) == "string" then theAsset.linked = {theAsset.linked} end for _, linkedGroup in pairs(theAsset.linked) do veaf.loggers.get(veafAssets.Id):trace(string.format("respawning linked group [%s]",linkedGroup)) mist.respawnGroup(linkedGroup, true) end end local text = "I've respawned " .. theAsset.description if theAsset.jtac then if ctld then veafSpawn.JTACAutoLase(name, theAsset.jtac, theAsset) text = text .. " lasing with code " .. theAsset.jtac end end trigger.action.outText(text, 30) end end function veafAssets.help(unitName) veaf.loggers.get(veafAssets.Id):trace(string.format("help(%s)",unitName or "")) local text = 'The radio menu lists all the assets, friendly or enemy\n' .. 'Use these menus to respawn the assets when needed\n' veaf.outTextForGroup(unitName, text, 30) end function veafAssets.get(assetName) return veafAssets.assets[assetName] end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafAssets.buildAssetsDatabase() veafAssets.assets = {} for _, asset in ipairs(veafAssets.Assets) do veafAssets.assets[asset.name] = asset end end function veafAssets.initialize() veafAssets.buildAssetsDatabase() veafAssets.buildRadioMenu() -- start any action-bound asset (e.g. jtacs) for name, asset in pairs(veafAssets.assets) do if asset.jtac then if ctld then veafSpawn.JTACAutoLase(name, asset.jtac, asset) end end end end veaf.loggers.get(veafAssets.Id):info(string.format("Loading version %s", veafAssets.Version)) --- Enable/Disable error boxes displayed on screen. env.setErrorMessageBoxEnabled(false) ------------------ END script veafAssets.lua ------------------ ------------------ START script veafCarrierOperations.lua ------------------ ------------------------------------------------------------------ -- VEAF carrier command and functions for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Radio menus allow starting and ending carrier operations. Carriers go back to their initial point when operations are ended -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafCarrierOperations = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafCarrierOperations.Id = "CARRIER" --- Version. veafCarrierOperations.Version = "1.12.2" -- trace level, specific to this module --veafCarrierOperations.LogLevel = "trace" veaf.loggers.new(veafCarrierOperations.Id, veafCarrierOperations.LogLevel) veafCarrierOperations.RadioMenuName = "CARRIER OPS" veafCarrierOperations.RadioMenuNameBlue = "CARRIER OPS - BLUE" veafCarrierOperations.RadioMenuNameRed = "CARRIER OPS - RED" veafCarrierOperations.DisableSecurity = false veafCarrierOperations.AllCarriers = { ["LHA_Tarawa"] = { runwayAngleWithBRC = -1, desiredWindSpeedOnDeck = 20}, ["Stennis"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["CVN_71"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["CVN_72"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["CVN_73"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["CVN_75"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["Forrestal"] = { runwayAngleWithBRC = 9.05, desiredWindSpeedOnDeck = 25}, ["KUZNECOW"] ={ runwayAngleWithBRC = 9, desiredWindSpeedOnDeck = 25}, ["CV_1143_5"] ={ runwayAngleWithBRC = 9, desiredWindSpeedOnDeck = 25} } veafCarrierOperations.ALT_FOR_MEASURING_WIND = 30 -- wind is measured at 30 meters, 10 meters above deck veafCarrierOperations.ALIGNMENT_MANOEUVER_SPEED = 20 * 0.51445 -- carrier speed when not yet aligned to the wind (in m/s) veafCarrierOperations.MAX_OPERATIONS_DURATION = 45 -- operations are stopped after (minutes) veafCarrierOperations.SCHEDULER_INTERVAL = 1 -- scheduler runs every minute veafCarrierOperations.MIN_WINDSPEED_FOR_CHANGING_HEADING = 4 * 0.51445 -- don't deroute the carrier if the wind speed is lower than this (m/s) veafCarrierOperations.MIN_CARRIER_SPEED = 4 * 0.51445 -- don't make the carrier steam at less than this speed (m/s) veafCarrierOperations.RemoteCommandParser = "([[a-zA-Z0-9]+)%s?([^%s]*)%s?(.*)" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Radio menus paths veafCarrierOperations.rootPath = nil veafCarrierOperations.rootPathBlue = nil veafCarrierOperations.rootPathRed = nil --- Carrier groups data, for Carrier Operations commands veafCarrierOperations.carriers = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafCarrierOperations.debugMarkersErasedAtEachStep = {} veafCarrierOperations.traceMarkerId = 2727 function veafCarrierOperations.getDebugMarkersErasedAtEachStep(name) if not name then return nil end if not veafCarrierOperations.debugMarkersErasedAtEachStep then veafCarrierOperations.debugMarkersErasedAtEachStep = {} end if not veafCarrierOperations.debugMarkersErasedAtEachStep[name] then veafCarrierOperations.debugMarkersErasedAtEachStep[name] = {} end return veafCarrierOperations.debugMarkersErasedAtEachStep[name] end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Carrier operations commands ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Start carrier operations ; changes the radio menu item to END and make the carrier move function veafCarrierOperations.startCarrierOperations(parameters) veaf.loggers.get(veafCarrierOperations.Id):debug("startCarrierOperations()") veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("Parameters for this command are : %s",veaf.p(parameters))) local carrierInfo, userUnitName = veaf.safeUnpack(parameters) local groupName, duration = veaf.safeUnpack(carrierInfo) veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("Carrier groupName : %s",veaf.p(groupName))) veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("duration : %s",veaf.p(duration))) veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("userUnitName : %s",veaf.p(userUnitName))) local carrier = veafCarrierOperations.carriers[groupName] if not(carrier) then local text = "Cannot find the carrier group "..groupName veaf.loggers.get(veafCarrierOperations.Id):error(text) veaf.outTextForGroup(userUnitName, text, 5) return end -- find the actual carrier unit local group = Group.getByName(groupName) for _, unit in pairs(group:getUnits()) do local unitType = unit:getDesc()["typeName"] for knownCarrierType, data in pairs(veafCarrierOperations.AllCarriers) do if unitType == knownCarrierType then carrier.carrierUnitName = unit:getName() carrier.pedroUnitName = carrier.carrierUnitName .. " Pedro" -- rescue helo unit name carrier.tankerUnitName = carrier.carrierUnitName .. " S3B-Tanker" -- emergency tanker unit name carrier.tankerRouteSet = 0 carrier.runwayAngleWithBRC = data.runwayAngleWithBRC carrier.desiredWindSpeedOnDeck = data.desiredWindSpeedOnDeck carrier.initialPosition = unit:getPosition().p veaf.loggers.get(veafCarrierOperations.Id):trace("initialPosition="..veaf.vecToString(carrier.initialPosition)) break end end end carrier.conductingAirOperations = true carrier.airOperationsStartedAt = timer.getTime() carrier.airOperationsEndAt = carrier.airOperationsStartedAt + duration * 60 veafCarrierOperations.continueCarrierOperations(groupName) -- will update the *carrier* structure local text = veafCarrierOperations.getAtcForCarrierOperations(groupName) .. "\n\nGetting a good alignment may require up to 5 minutes" veaf.loggers.get(veafCarrierOperations.Id):info(text) veaf.outTextForGroup(userUnitName, text, 25) -- change the menu veaf.loggers.get(veafCarrierOperations.Id):trace("change the menu") veafCarrierOperations.rebuildRadioMenu() end --- Continue carrier operations ; make the carrier move according to the wind. Called by startCarrierOperations and by the scheduler. function veafCarrierOperations.continueCarrierOperations(groupName, userUnitName) veaf.loggers.get(veafCarrierOperations.Id):debug("continueCarrierOperations(".. groupName .. ")") local carrier = veafCarrierOperations.carriers[groupName] if not(carrier) then local text = "Cannot find the carrier group "..groupName veaf.loggers.get(veafCarrierOperations.Id):error(text) veaf.outTextForGroup(userUnitName, text, 5) return end -- find the actual carrier unit local carrierUnit = Unit.getByName(carrier.carrierUnitName) -- take note of the starting position local startPosition = veaf.getAvgGroupPos(groupName) local currentHeading = 0 if carrierUnit then startPosition = carrierUnit:getPosition().p veaf.loggers.get(veafCarrierOperations.Id):trace("startPosition (raw) ="..veaf.vecToString(startPosition)) currentHeading = mist.utils.round(mist.utils.toDegree(mist.getHeading(carrierUnit, true)), 0) end veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("currentHeading=%s", veaf.p(currentHeading))) startPosition = { x=startPosition.x, z=startPosition.z, y=startPosition.y + veafCarrierOperations.ALT_FOR_MEASURING_WIND} -- on deck, 50 meters above the water veaf.loggers.get(veafCarrierOperations.Id):trace("startPosition="..veaf.vecToString(startPosition)) veaf.loggers.get(veafCarrierOperations.Id):cleanupMarkers(veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) --veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", "startPosition", startPosition, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) local carrierDistanceFromInitialPosition = ((startPosition.x - carrier.initialPosition.x)^2 + (startPosition.z - carrier.initialPosition.z)^2)^0.5 veaf.loggers.get(veafCarrierOperations.Id):trace("carrierDistanceFromInitialPosition="..carrierDistanceFromInitialPosition) -- compute magnetic deviation at carrier position -- let's not use mist.getNorthCorrection, it's not computing magnetic deviation... -- TODO find how to actually compute it --[[ local magdev = veaf.round(mist.getNorthCorrection(startPosition) * 180 / math.pi,1) veaf.loggers.get(veafCarrierOperations.Id):trace("magdev = " .. magdev) ]] -- make the carrier move if startPosition ~= nil then local dir = currentHeading -- start with current heading --get wind info local wind = atmosphere.getWind(startPosition) veaf.loggers.get(veafCarrierOperations.Id):trace("wind=%s", veaf.p(wind)) local windspeed = mist.vec.mag(wind) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("windspeed=%s", veaf.p(windspeed))) if windspeed >= veafCarrierOperations.MIN_WINDSPEED_FOR_CHANGING_HEADING then --get wind direction sorted if wind.x ~= 0 then dir = veaf.round(math.atan2(wind.z, wind.x) * 180 / math.pi,0) elseif wind.z < 0 then dir = 270 elseif wind.z > 0 then dir = 90 elseif wind.z == 0 then dir = carrier.heading end if dir < 0 then dir = dir + 360 --converts to positive numbers end if dir <= 180 then dir = dir + 180 else dir = dir - 180 end veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("wind direction=%s", veaf.p(dir))) dir = veaf.round(dir + carrier.runwayAngleWithBRC) --to account for angle of landing deck and movement of the ship end if dir > 360 then dir = dir - 360 end veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("dir=%s", veaf.p(dir))) local speed = 1 local desiredWindSpeedOnDeck = carrier.desiredWindSpeedOnDeck * 0.51445 if desiredWindSpeedOnDeck < 1 then desiredWindSpeedOnDeck = 1 end -- minimum 1 m/s if windspeed < desiredWindSpeedOnDeck then speed = desiredWindSpeedOnDeck - windspeed end if speed < veafCarrierOperations.MIN_CARRIER_SPEED then speed = veafCarrierOperations.MIN_CARRIER_SPEED end veaf.loggers.get(veafCarrierOperations.Id):trace("BRC speed="..speed.." m/s") -- compute a new waypoint local headingRad = mist.utils.toRadian(dir) local length = 4000 local newWaypoint = { x = startPosition.x + length * math.cos(headingRad), z = startPosition.z + length * math.sin(headingRad), y = startPosition.y } -- check for obstructions local carrierGroup = Group.getByName(groupName) local unitsToCheck = {} if carrierGroup then for _, unitToCheck in pairs(carrierGroup:getUnits()) do veaf.loggers.get(veafCarrierOperations.Id):trace("checking %s %s", veaf.p(unitToCheck:getTypeName()), veaf.p(unitToCheck:getName())) if not carrierUnit or unitToCheck:getID() ~= carrierUnit:getID() then table.insert(unitsToCheck, unitToCheck) end end end veaf.loggers.get(veafCarrierOperations.Id):trace("unitsToCheck=%s", veaf.p(unitsToCheck)) local pointA = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 500, 500) local pointB = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 500, -500) local pointC = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 2000, -500) local pointD = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 2000, 500) local polygon = {pointA, pointB, pointC, pointD} veaf.loggers.get(veafCarrierOperations.Id):trace("polygon=%s", veaf.p(polygon)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):markerQuad(veafCarrierOperations.traceMarkerId, "CARRIER", "obstructionsCheck", {pointA, pointB, pointC, pointD}, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName), VeafDrawingOnMap.LINE_TYPE["dashed"], {1, 0, 0, 0.5}) local obstructions = {} for i =1, #unitsToCheck do local lUnit = unitsToCheck[i] veaf.loggers.get(veafCarrierOperations.Id):trace("lUnit:getName()=%s", veaf.p(lUnit:getName())) if mist.pointInPolygon(lUnit:getPosition().p, polygon) then obstructions[#obstructions + 1] = lUnit end end veaf.loggers.get(veafCarrierOperations.Id):trace("obstructions=%s", veaf.p(obstructions)) if #obstructions > 0 then -- obstructions found, derouting local newDir = dir + 90 if newDir > 360 then newDir = newDir - 360 end local msg = string.format("Obstruction found at heading %s, derouting %s to heading %s", veaf.p(#obstructions), veaf.p(dir), veaf.p(groupName), veaf.p(newDir)) veaf.loggers.get(veafCarrierOperations.Id):debug(msg) veaf.outTextForGroup(userUnitName, msg, 5) headingRad = mist.utils.toRadian(newDir) length = 4000 newWaypoint = { x = startPosition.x + length * math.cos(headingRad), z = startPosition.z + length * math.sin(headingRad), y = startPosition.y } end veaf.loggers.get(veafCarrierOperations.Id):trace("headingRad="..headingRad) veaf.loggers.get(veafCarrierOperations.Id):trace("length="..length) veaf.loggers.get(veafCarrierOperations.Id):trace("newWaypoint="..veaf.vecToString(newWaypoint)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):markerArrow(veafCarrierOperations.traceMarkerId, "CARRIER", "route", startPosition, newWaypoint, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName), VeafDrawingOnMap.LINE_TYPE["dashed"], {0, 0, 1, 0.3}) local actualSpeed = speed if math.abs(dir - currentHeading) > 15 then -- still aligning actualSpeed = veafCarrierOperations.ALIGNMENT_MANOEUVER_SPEED end veaf.moveGroupTo(groupName, newWaypoint, actualSpeed, 0) carrier.heading = dir veaf.loggers.get(veafCarrierOperations.Id):trace("carrier.heading = " .. carrier.heading .. " (true)") --carrier.heading_mag = dir + magdev --veaf.loggers.get(veafCarrierOperations.Id):trace("carrier.heading = " .. carrier.heading_mag .. " (mag)") carrier.speed = veaf.round(speed * 1.94384, 0) veaf.loggers.get(veafCarrierOperations.Id):trace("carrier.speed = " .. carrier.speed .. " kn") -- check if a Pedro group exists for this carrier if not(mist.DBs.groupsByName[carrier.pedroUnitName]) then veaf.loggers.get(veafCarrierOperations.Id):warn("No Pedro group named " .. carrier.pedroUnitName) else -- prepare or correct the Pedro route (SH-60B, 250ft high, 1nm to the starboard side of the carrier, riding along at the same speed and heading) local pedroUnit = Unit.getByName(carrier.pedroUnitName) if (pedroUnit) then veaf.loggers.get(veafCarrierOperations.Id):debug("found Pedro unit") -- check if unit is still alive if pedroUnit:getLife() < 1 then pedroUnit = nil -- respawn when damaged end end -- spawn if needed if not(pedroUnit and carrier.pedroIsSpawned) then veaf.loggers.get(veafCarrierOperations.Id):debug("respawning Pedro unit") local vars = {} vars.gpName = carrier.pedroUnitName vars.action = 'respawn' vars.point = startPosition vars.point.y = 100 vars.radius = 500 mist.teleportToPoint(vars) carrier.pedroIsSpawned = true end local pedroGroup = Group.getByName(carrier.pedroUnitName) -- group has the same name as the unit if (pedroGroup) then veaf.loggers.get(veafCarrierOperations.Id):debug("found Pedro group") pedroUnit = Unit.getByName(carrier.pedroUnitName) if not pedroUnit then pedroUnit = pedroGroup:getUnits(1) end veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("pedroUnit=%s",veaf.p(pedroUnit))) -- waypoint #1 is 500m to port local offsetPointOnLand, offsetPoint = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 0, 500) local pedroWaypoint1 = offsetPoint local distanceFromWP1 = ((pedroUnit:getPosition().p.x - pedroWaypoint1.x)^2 + (pedroUnit:getPosition().p.z - pedroWaypoint1.z)^2)^0.5 if distanceFromWP1 > 500 then veaf.loggers.get(veafCarrierOperations.Id):trace("Pedro WP1 = " .. veaf.vecToString(pedroWaypoint1)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", "pedroWaypoint1", pedroWaypoint1, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) else pedroWaypoint1 = nil end -- waypoint #2 is 500m to port, near the end of the carrier route local offsetPointOnLand, offsetPoint = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, length - 250, 500) local pedroWaypoint2 = offsetPoint veaf.loggers.get(veafCarrierOperations.Id):trace("Pedro WP2 = " .. veaf.vecToString(pedroWaypoint2)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", "pedroWaypoint2", pedroWaypoint2, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) local mission = { id = 'Mission', params = { ["communication"] = false, ["start_time"] = 0, ["task"] = "Transport", route = { points = { } } } } if pedroWaypoint1 then mission.params.route.points = { [1] = { ["alt"] = 35, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 50, ["type"] = "Turning Point", ["x"] = pedroUnit:getPosition().p.x, ["y"] = pedroUnit:getPosition().p.z, ["speed_locked"] = true, }, [2] = { ["type"] = "Turning Point", ["action"] = "Turning Point", ["x"] = pedroWaypoint1.x, ["y"] = pedroWaypoint1.z, ["alt"] = 35, -- in meters ["alt_type"] = "BARO", ["speed"] = 50, ["speed_locked"] = true, }, [3] = { ["type"] = "Turning Point", ["action"] = "Turning Point", ["x"] = pedroWaypoint2.x, ["y"] = pedroWaypoint2.z, ["alt"] = 35, -- in meters ["alt_type"] = "BARO", ["speed"] = speed, -- speed in m/s ["speed_locked"] = true, }, } else mission.params.route.points = { [1] = { ["alt"] = 35, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 50, ["type"] = "Turning Point", ["x"] = pedroUnit:getPosition().p.x, ["y"] = pedroUnit:getPosition().p.z, ["speed_locked"] = true, }, [2] = { ["type"] = "Turning Point", ["action"] = "Turning Point", ["x"] = pedroWaypoint2.x, ["y"] = pedroWaypoint2.z, ["alt"] = 35, -- in meters ["alt_type"] = "BARO", ["speed"] = speed, -- speed in m/s ["speed_locked"] = true, }, } end -- replace whole mission veaf.loggers.get(veafCarrierOperations.Id):debug("Setting Pedro mission") local controller = pedroGroup:getController() controller:setTask(mission) end end -- check if a S3B-Tanker group exists for this carrier if not(mist.DBs.groupsByName[carrier.tankerUnitName]) then veaf.loggers.get(veafCarrierOperations.Id):warn("No Tanker group named " .. carrier.tankerUnitName) else local routeTanker = (carrierDistanceFromInitialPosition > 18520) carrier.tankerRouteSet = carrier.tankerRouteSet + 1 if carrier.tankerRouteSet <= 2 then -- prepare or correct the Tanker route (8000ft high, 10nm aft and 4nm to the starboard side of the carrier, refueling on BRC) local tankerUnit = Unit.getByName(carrier.tankerUnitName) if (tankerUnit) then veaf.loggers.get(veafCarrierOperations.Id):debug("found Tanker unit") -- check if unit is still alive if tankerUnit:getLife() < 1 then tankerUnit = nil -- respawn when damaged end end -- spawn if needed if not(tankerUnit and carrier.tankerIsSpawned) then veaf.loggers.get(veafCarrierOperations.Id):debug("respawning Tanker unit") local vars = {} vars.gpName = carrier.tankerUnitName vars.action = 'respawn' vars.point = {} vars.point.x = startPosition.x vars.point.z = startPosition.z vars.point.y = 2500 vars.radius = 500 mist.teleportToPoint(vars) carrier.tankerIsSpawned = true end tankerUnit = Unit.getByName(carrier.tankerUnitName) local tankerGroup = Group.getByName(carrier.tankerUnitName) -- group has the same name as the unit if (tankerGroup) then veaf.loggers.get(veafCarrierOperations.Id):debug("found Tanker group") veaf.loggers.get(veafCarrierOperations.Id):trace("groupName="..tankerGroup:getName()) -- waypoint #1 is 5nm to port, 5nm to the front local offsetPointOnLand, offsetPoint = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 9000, 9000) local tankerWaypoint1 = offsetPoint veaf.loggers.get(veafCarrierOperations.Id):trace("Tanker WP1 = " .. veaf.vecToString(tankerWaypoint1)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", "tankerWaypoint1", tankerWaypoint1, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) -- waypoint #2 is 20nm ahead of waypoint #2, on BRC local offsetPointOnLand, offsetPoint = veaf.computeCoordinatesOffsetFromRoute(startPosition, newWaypoint, 37000 + 9000, 9000) local tankerWaypoint2 = offsetPoint veaf.loggers.get(veafCarrierOperations.Id):trace("Tanker WP2 = " .. veaf.vecToString(tankerWaypoint2)) veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", "tankerWaypoint2", tankerWaypoint2, veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) local mission = { id = 'Mission', params = { ["communication"] = true, ["start_time"] = 0, ["task"] = "Refueling", ["taskSelected"] = true, ["route"] = { ["points"] = { [1] = { ["alt"] = 2500, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 165, ["type"] = "Turning Point", ["x"] = startPosition.x, ["y"] = startPosition.z, ["speed_locked"] = true, }, [2] = { ["alt"] = 2500, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 165, ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { [1] = { ["enabled"] = true, ["auto"] = true, ["id"] = "Tanker", ["number"] = 1, }, -- end of [1] [2] = carrier.tankerData.tankerTacanTask }, -- end of ["tasks"] }, -- end of ["params"] }, -- end of ["task"] ["type"] = "Turning Point", ["ETA"] = 0, ["ETA_locked"] = false, ["x"] = startPosition.x, ["y"] = startPosition.z, ["speed_locked"] = true, }, [3] = { ["alt"] = 2500, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 165, ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { [1] = { ["enabled"] = true, ["auto"] = false, ["id"] = "Orbit", ["number"] = 1, ["params"] = { ["altitude"] = 2500, ["pattern"] = "Race-Track", ["speed"] = 165, }, -- end of ["params"] }, -- end of [1] }, -- end of ["tasks"] }, -- end of ["params"] }, -- end of ["task"] ["type"] = "Turning Point", ["x"] = tankerWaypoint1.x, ["y"] = tankerWaypoint1.z, ["speed_locked"] = true, }, [4] = { ["alt"] = 2500, ["action"] = "Turning Point", ["alt_type"] = "BARO", ["speed"] = 165, ["type"] = "Turning Point", ["x"] = tankerWaypoint2.x, ["y"] = tankerWaypoint2.z, ["speed_locked"] = true, }, -- end of [3] }, -- end of ["points"] }, -- end of ["route"] } } -- replace whole mission veaf.loggers.get(veafCarrierOperations.Id):debug("Setting Tanker mission") local controller = tankerGroup:getController() controller:setTask(mission) carrier.tankerRouteIsSet = true local _setFrequency = { id = 'SetFrequency', params = { frequency = carrier.tankerData.tankerFrequency * 1000000, --Hz modulation = 0 --AM } } Controller.setCommand(controller, _setFrequency) end end end end end --- Gets informations about current carrier operations function veafCarrierOperations.getAtcForCarrierOperations(groupName, skipNavigationData) veaf.loggers.get(veafCarrierOperations.Id):debug("getAtcForCarrierOperations(".. groupName .. ")") local carrier = veafCarrierOperations.carriers[groupName] local carrierUnit = Unit.getByName(carrier.carrierUnitName) local currentHeading = -1 local currentSpeed = -1 local startPosition = nil if carrierUnit then currentHeading = mist.utils.round(mist.utils.toDegree(mist.getHeading(carrierUnit, true)), 0) currentSpeed = mist.utils.round(mist.utils.mpsToKnots(mist.vec.mag(carrierUnit:getVelocity())),0) startPosition = { x=carrierUnit:getPosition().p.x, z=carrierUnit:getPosition().p.z, y=veafCarrierOperations.ALT_FOR_MEASURING_WIND} -- on deck, 50 meters above the water end if not(carrier) then local text = "Cannot find the carrier group "..groupName veaf.loggers.get(veafCarrierOperations.Id):error(text) trigger.action.outText(text, 5) return end local result = "" local groupPosition = veaf.getAvgGroupPos(groupName) if carrier.conductingAirOperations then local remainingTime = veaf.round((carrier.airOperationsEndAt - timer.getTime()) /60, 1) result = "The carrier group "..groupName.." is conducting air operations :\n" if carrier.ATC.tower then result = result .. " - ATC : " .. carrier.ATC.tower .. "\n" end if carrier.ATC.tacan then result = result .. " - TACAN : " .. carrier.ATC.tacan .. "\n" end if carrier.ATC.icls then result = result .. " - ICLS : " .. carrier.ATC.icls .. "\n" end if carrier.ATC.link4 then result = result .. " - LINK 4 : " .. carrier.ATC.link4 .. ", " if carrier.ATC.acls then result = result .. "ACLS is available" end result = result .. "\n" end --" - BRC : " .. carrier.heading_mag .. " (".. carrier.heading .. " true) at " .. carrier.speed .. " kn\n" .. result = result .. "\n - BRC : " .. carrier.heading .. " (true) at " .. carrier.speed .. " kn\n" .. " - Remaining time : " .. remainingTime .. " minutes\n" if carrier.tankerData then result = result .. "\n - Tanker " .. carrier.tankerData.tankerCallsign .. " : TACAN " ..carrier.tankerData.tankerTacanChannel.. carrier.tankerData.tankerTacanMode ..", COMM " .. carrier.tankerData.tankerFrequency .. "\n" end else result = "The carrier group "..groupName.." is not conducting carrier air operations\n" end if not(skipNavigationData) then -- add current navigation data if currentHeading > -1 and currentSpeed > -1 then -- compute magnetic deviation at carrier position -- let's not use mist.getNorthCorrection, it's not computing magnetic deviation... -- TODO find how to actually compute it --[[ local magdev = veaf.round(mist.getNorthCorrection(startPosition) * 180 / math.pi,1) veaf.loggers.get(veafCarrierOperations.Id):trace("magdev = " .. magdev) ]] result = result .. "\n".. "Current navigation parameters :\n" .. " - Current heading (true) " .. veaf.round(currentHeading, 0) .. "\n" .. --" - Current heading (mag) " .. veaf.round(currentHeading, 0) .. "\n" .. " - Current speed " .. currentSpeed .. " kn\n" end end result = result .. "\n"..veaf.weatherReport(startPosition) return result end --- Gets informations about current carrier operations function veafCarrierOperations.atcForCarrierOperations(parameters) local groupName, unitName = veaf.safeUnpack(parameters) veaf.loggers.get(veafCarrierOperations.Id):debug("atcForCarrierOperations(".. groupName .. ")") local text = veafCarrierOperations.getAtcForCarrierOperations(groupName) veaf.outTextForGroup(unitName, text, 15) end --- Ends carrier operations ; changes the radio menu item to START and send the carrier back to its starting point function veafCarrierOperations.stopCarrierOperations(parameters) local groupName, userUnitName = veaf.safeUnpack(parameters) veaf.loggers.get(veafCarrierOperations.Id):debug("stopCarrierOperations(".. groupName .. ")") local carrier = veafCarrierOperations.carriers[groupName] if not(carrier) then local text = "Cannot find the carrier group "..groupName veaf.loggers.get(veafCarrierOperations.Id):error(text) trigger.action.outText(text, 5) return end local carrierUnit = Unit.getByName(carrier.carrierUnitName) local carrierPosition = carrierUnit:getPosition().p local text = "The carrier group "..groupName.." has stopped air operations ; it's moving back to its initial position" veaf.loggers.get(veafCarrierOperations.Id):info(text) veaf.outTextForGroup(userUnitName, text, 5) carrier.conductingAirOperations = false carrier.stoppedAirOperations = true -- change the menu veaf.loggers.get(veafCarrierOperations.Id):trace("change the menu") veafCarrierOperations.rebuildRadioMenu() -- make the Pedro land if (carrier.pedroIsSpawned) then carrier.pedroIsSpawned = false local pedroUnit = Unit.getByName(carrier.pedroUnitName) if (pedroUnit) then veaf.loggers.get(veafCarrierOperations.Id):debug("found Pedro unit ; destroying it") pedroUnit:destroy() end end -- make the tanker land if (carrier.tankerIsSpawned) then carrier.tankerIsSpawned = false local tankerUnit = Unit.getByName(carrier.tankerUnitName) if (tankerUnit) then veaf.loggers.get(veafCarrierOperations.Id):debug("found tanker unit ; destroying it") tankerUnit:destroy() end end veafCarrierOperations.doOperations() end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Rebuild the radio menu function veafCarrierOperations.rebuildRadioMenu() veaf.loggers.get(veafCarrierOperations.Id):debug("veafCarrierOperations.rebuildRadioMenu()") -- find the carriers in the veafCarrierOperations.carriers table and prepare their menus for name, carrier in pairs(veafCarrierOperations.carriers) do veaf.loggers.get(veafCarrierOperations.Id):trace("rebuildRadioMenu processing "..name) local menuRoot = veafCarrierOperations.rootPathRed if carrier.side == coalition.side.BLUE then menuRoot = veafCarrierOperations.rootPathBlue end -- remove the submenu if it exists if carrier.menuPath then veafRadio.delSubmenu(carrier.menuPath, menuRoot) veaf.loggers.get(veafCarrierOperations.Id):trace("remove the submenu") end -- create the submenu veaf.loggers.get(veafCarrierOperations.Id):trace("create the submenu") carrier.menuPath = veafRadio.addSubMenu(name, menuRoot) if carrier.conductingAirOperations then -- add the stop menu if veafCarrierOperations.DisableSecurity then veafRadio.addCommandToSubmenu("End air operations", carrier.menuPath, veafCarrierOperations.stopCarrierOperations, name, veafRadio.USAGE_ForGroup) else veafRadio.addSecuredCommandToSubmenu("End air operations", carrier.menuPath, veafCarrierOperations.stopCarrierOperations, name, veafRadio.USAGE_ForGroup) end else -- add the "start for veafCarrierOperations.MAX_OPERATIONS_DURATION" menu local startMenuName1 = "Start carrier air operations for " .. veafCarrierOperations.MAX_OPERATIONS_DURATION .. " minutes" if veafCarrierOperations.DisableSecurity then veafRadio.addCommandToSubmenu(startMenuName1, carrier.menuPath, veafCarrierOperations.startCarrierOperations, { name, veafCarrierOperations.MAX_OPERATIONS_DURATION }, veafRadio.USAGE_ForGroup) else veafRadio.addSecuredCommandToSubmenu(startMenuName1, carrier.menuPath, veafCarrierOperations.startCarrierOperations, { name, veafCarrierOperations.MAX_OPERATIONS_DURATION }, veafRadio.USAGE_ForGroup) end -- add the "start for veafCarrierOperations.MAX_OPERATIONS_DURATION * 2" menu local startMenuName2 = "Start carrier air operations for " .. veafCarrierOperations.MAX_OPERATIONS_DURATION * 2 .. " minutes" if veafCarrierOperations.DisableSecurity then veafRadio.addCommandToSubmenu(startMenuName2, carrier.menuPath, veafCarrierOperations.startCarrierOperations, { name, veafCarrierOperations.MAX_OPERATIONS_DURATION * 2 }, veafRadio.USAGE_ForGroup) else veafRadio.addSecuredCommandToSubmenu(startMenuName2, carrier.menuPath, veafCarrierOperations.startCarrierOperations, { name, veafCarrierOperations.MAX_OPERATIONS_DURATION * 2 }, veafRadio.USAGE_ForGroup) end end -- add the ATC menu (by player group) veafRadio.addCommandToSubmenu("ATC - Request informations", carrier.menuPath, veafCarrierOperations.atcForCarrierOperations, name, veafRadio.USAGE_ForGroup) veafRadio.refreshRadioMenu() end end --- Build the initial radio menu function veafCarrierOperations.buildRadioMenu() veaf.loggers.get(veafCarrierOperations.Id):debug("veafCarrierOperations.buildRadioMenu") -- don't create an empty menu if veaf.length(veafCarrierOperations.carriers) == 0 then return end veafCarrierOperations.rootPath = veafRadio.addSubMenu(veafCarrierOperations.RadioMenuName) veafCarrierOperations.rootPathBlue = veafRadio.addSubMenu(veafCarrierOperations.RadioMenuNameBlue, veafCarrierOperations.rootPath) veafCarrierOperations.rootPathRed = veafRadio.addSubMenu(veafCarrierOperations.RadioMenuNameRed, veafCarrierOperations.rootPath) -- build HELP menu for each group if not(veafRadio.skipHelpMenus) then veafRadio.addCommandToSubmenu("HELP", veafCarrierOperations.rootPath, veafCarrierOperations.help, nil, veafRadio.USAGE_ForGroup) end veafCarrierOperations.rebuildRadioMenu() end function veafCarrierOperations.help(unitName) local text = 'Use the radio menus to start and end carrier operations\n' .. 'START: carrier will find out the wind and set sail at optimum speed to achieve a 25kn headwind\n' .. ' the radio menu will show the recovery course and TACAN information\n' .. 'END : carrier will go back to its starting point (where it was when the START command was issued)\n' .. 'RESET: carrier will go back to where it was when the mission started' veaf.outTextForGroup(unitName, text, 30) end function veafCarrierOperations.initializeCarrierGroups() -- find the carriers and add them to the veafCarrierOperations.carriers table, store its initial location and create the menus for name, group in pairs(mist.DBs.groupsByName) do veaf.loggers.get(veafCarrierOperations.Id):trace("found group "..name) -- search groups with a carrier unit in the group local carrier = nil -- find the actual carrier unit local group = Group.getByName(name) if group then for _, unit in pairs(group:getUnits()) do local unitType = unit:getDesc()["typeName"] for knownCarrierType, data in pairs(veafCarrierOperations.AllCarriers) do if unitType == knownCarrierType then local coa = group:getCoalition() veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("coa=%s", veaf.p(coa))) -- found a carrier, initialize the carrier group object if needed if not carrier then veafCarrierOperations.carriers[name] = {} carrier = veafCarrierOperations.carriers[name] veaf.loggers.get(veafCarrierOperations.Id):trace("found carrier !") else veaf.loggers.get(veafCarrierOperations.Id):warn(string.format("more than one carrier in group %s", veaf.p(name))) end carrier.side = coa carrier.carrierUnit = unit carrier.carrierUnitName = carrier.carrierUnit:getName() carrier.runwayAngleWithBRC = data.runwayAngleWithBRC carrier.desiredWindSpeedOnDeck = data.desiredWindSpeedOnDeck carrier.heading = mist.getHeading(unit, true) --veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("Carrier Data from MIST : %s",veaf.p(veaf.getGroupData(name)))) carrier.ATC = {} carrier.ATC = veaf.getCarrierATCdata(name, unit:getName()) carrier.pedroUnitName = carrier.carrierUnitName .. " Pedro" -- rescue helo unit name local pedroUnit = Unit.getByName(carrier.pedroUnitName) if pedroUnit then pedroUnit:destroy() end carrier.tankerUnitName = carrier.carrierUnitName .. " S3B-Tanker" -- emergency tanker unit name carrier.tankerData = veaf.getTankerData(carrier.tankerUnitName) local tankerUnit = Unit.getByName(carrier.tankerUnitName) if tankerUnit then tankerUnit:destroy() end break end end end if carrier then -- take note of the carrier route carrier.missionRoute = mist.getGroupRoute(name, 'task') veaf.loggers.get(veafCarrierOperations.Id):trace("carrier.missionRoute=%s", veaf.p(carrier.missionRoute)) if veafCarrierOperations.Trace then for num, point in pairs(carrier.missionRoute) do veafCarrierOperations.traceMarkerId = veaf.loggers.get(veafCarrierOperations.Id):marker(veafCarrierOperations.traceMarkerId, "CARRIER", string.format("[%s] point %d", name, tostring(num)), point, nil) end end end end end end function veafCarrierOperations.doOperations() veaf.loggers.get(veafCarrierOperations.Id):debug("veafCarrierOperations.doOperations()") -- find the carriers in the veafCarrierOperations.carriers table and check if they are operating for name, carrier in pairs(veafCarrierOperations.carriers) do veaf.loggers.get(veafCarrierOperations.Id):debug("checking " .. name) if carrier.conductingAirOperations then veaf.loggers.get(veafCarrierOperations.Id):debug(name .. " is conducting operations ; checking course and ops duration") if carrier.airOperationsEndAt < timer.getTime() then -- time to stop operations veaf.loggers.get(veafCarrierOperations.Id):info(name .. " has been conducting operations long enough ; stopping ops") veafCarrierOperations.stopCarrierOperations(name) else local remainingTime = veaf.round((carrier.airOperationsEndAt - timer.getTime()) /60, 1) veaf.loggers.get(veafCarrierOperations.Id):debug(name .. " will continue conducting operations for " .. remainingTime .. " more minutes") -- check and reset course veafCarrierOperations.continueCarrierOperations(name) end elseif carrier.stoppedAirOperations then carrier.conductingAirOperations = false veaf.loggers.get(veafCarrierOperations.Id):debug(name .. " stopped conducting operations") veaf.loggers.get(veafCarrierOperations.Id):cleanupMarkers(veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) carrier.stoppedAirOperations = false -- reset the carrier group route to its original route (set in the mission) if carrier.missionRoute then veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("resetting carrier %s route", name)) veaf.loggers.get(veafCarrierOperations.Id):trace("carrier.missionRoute="..veaf.p(carrier.missionRoute)) local result = mist.goRoute(name, carrier.missionRoute) end else veaf.loggers.get(veafCarrierOperations.Id):debug(name .. " is not conducting operations") end end end --- This function is called at regular interval (see veafCarrierOperations.SCHEDULER_INTERVAL) and manages the carrier operations schedules --- It will make any carrier group that has started carrier operations maintain a correct course for recovery, even if wind changes. --- Also, it will stop carrier operations after a set time (see veafCarrierOperations.MAX_OPERATIONS_DURATION). function veafCarrierOperations.operationsScheduler() veaf.loggers.get(veafCarrierOperations.Id):debug("veafCarrierOperations.operationsScheduler()") veafCarrierOperations.doOperations() veaf.loggers.get(veafCarrierOperations.Id):debug("veafCarrierOperations.operationsScheduler() - rescheduling in " .. veafCarrierOperations.SCHEDULER_INTERVAL * 60 .. " s") mist.scheduleFunction(veafCarrierOperations.operationsScheduler,{},timer.getTime() + veafCarrierOperations.SCHEDULER_INTERVAL * 60) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafCarrierOperations.listAvailableCarriers(forGroup) local _message = "Available carriers :\n" for name, carrier in pairs(veafCarrierOperations.carriers) do _message = _message .. " - " .. name .. "\n" end if forGroup then trigger.action.outTextForGroup(forGroup, _message, 15) else trigger.action.outText(_message, 15) end end -- execute command from the remote interface function veafCarrierOperations.executeCommandFromRemote(parameters) veaf.loggers.get(veafCarrierOperations.Id):debug(string.format("veafCarrierOperations.executeCommandFromRemote()")) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_pilot= %s", veaf.p(_pilot))) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName))) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_unitName= %s", veaf.p(_unitName))) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_command= %s", veaf.p(_command))) if not _pilot or not _command then return false end local function findCarrier(carrierName) local _result = nil local _name = carrierName:lower() for name, carrier in pairs(veafCarrierOperations.carriers) do if name:lower():find(_name) then _result = name end end return _result end if _command then -- parse the command local _action, _carrierName, _parameters = _command:match(veafCarrierOperations.RemoteCommandParser) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_action=%s",veaf.p(_action))) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_carrierName=%s",veaf.p(_carrierName))) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_parameters=%s",veaf.p(_parameters))) local _groupId = nil if _unitName then local _unit = Unit.getByName(_unitName) if _unit then _groupId = _unit:getGroup():getID() end end veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_groupId=%s",veaf.p(_groupId))) if _action and _action:lower() == "list" then veaf.loggers.get(veafCarrierOperations.Id):info(string.format("[%s] is listing carriers)",veaf.p(_pilot.name))) veafCarrierOperations.listAvailableCarriers(_groupId) return true elseif _action and _action:lower() == "start" and _carrierName then local _duration = 45 if _parameters and type(_parameters) == "number" then _duration = tonumber(parameters) end local _carrier = findCarrier(_carrierName) veaf.loggers.get(veafCarrierOperations.Id):trace(string.format("_duration=%s",veaf.p(_duration))) veaf.loggers.get(veafCarrierOperations.Id):info(string.format("[%s] is starting operations on carrier [%s] for %s)",veaf.p(_pilot.name), veaf.p(_carrier), veaf.p(_parameters))) veafCarrierOperations.startCarrierOperations({_carrier, _duration}) return true elseif _action and _action:lower() == "stop" then local _carrier = findCarrier(_carrierName) veaf.loggers.get(veafCarrierOperations.Id):info(string.format("[%s] is stopping operations on carrier [%s])",veaf.p(_pilot.name), veaf.p(_carrier))) veafCarrierOperations.stopCarrierOperations(_carrier) return true elseif _action and _action:lower() == "atc" then local _carrier = findCarrier(_carrierName) veaf.loggers.get(veafCarrierOperations.Id):info(string.format("[%s] is requesting atc on carrier [%s])",veaf.p(_pilot.name), veaf.p(_carrier))) local text = veafCarrierOperations.getAtcForCarrierOperations(_carrier) if _groupId then trigger.action.outTextForGroup(_groupId, text, 15) else trigger.action.outText(text, 15) end return true end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Carrier ATC ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafCarrierOperations.initialize() veafCarrierOperations.initializeCarrierGroups() veafCarrierOperations.buildRadioMenu() veafCarrierOperations.operationsScheduler() end veaf.loggers.get(veafCarrierOperations.Id):info(string.format("Loading version %s", veafCarrierOperations.Version)) --- Enable/Disable error boxes displayed on screen. env.setErrorMessageBoxEnabled(false) ------------------ END script veafCarrierOperations.lua ------------------ ------------------ START script veafCasMission.lua ------------------ ------------------------------------------------------------------ -- VEAF CAS (Close Air Support) command and functions for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Listen to marker change events and creates a CAS training mission, with optional parameters -- * Create a CAS target group, protected by SAM, AAA and manpads, to use for CAS training -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafCasMission = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafCasMission.Id = "CASMISSION" --- Version. veafCasMission.Version = "1.15.2" -- trace level, specific to this module --veafCasMission.LogLevel = "trace" veaf.loggers.new(veafCasMission.Id, veafCasMission.LogLevel) --- Key phrase to look for in the mark text which triggers the command. veafCasMission.Keyphrase = "_cas" --- Number of seconds between each check of the CAS group watchdog function veafCasMission.SecondsBetweenWatchdogChecks = 15 --- Number of seconds between each smoke request on the CAS targets group veafCasMission.SecondsBetweenSmokeRequests = 180 --- Number of seconds between each flare request on the CAS targets group veafCasMission.SecondsBetweenFlareRequests = 120 --- Name of the CAS targets vehicles group veafCasMission.RedCasGroupName = "Red CAS Group" veafCasMission.BlueCasGroupName = "Blue CAS Group" veafCasMission.casGroupName = veafCasMission.RedCasGroupName veafCasMission.afacName = nil veafCasMission.RadioMenuName = "CAS MISSION" veafCasMission.TRANSPORT_TYPES = { [coalition.side.BLUE] = { [veaf.ERA.MODERN] = { [0] = {"LUV HMMWV Jeep", "M 818", "M978 HEMTT Tanker", "Land_Rover_101_FC", "Land_Rover_109_S3"}, [1] = {"LUV HMMWV Jeep", "M 818", "M978 HEMTT Tanker", "Land_Rover_101_FC", "Land_Rover_109_S3"}, [2] = {"LUV HMMWV Jeep", "M 818", "M978 HEMTT Tanker", "Land_Rover_101_FC", "Land_Rover_109_S3"}, [3] = {"LUV HMMWV Jeep", "M 818", "M978 HEMTT Tanker", "Land_Rover_101_FC", "Land_Rover_109_S3"}, [4] = {"LUV HMMWV Jeep", "M 818", "M978 HEMTT Tanker", "Land_Rover_101_FC", "Land_Rover_109_S3"}, [5] = {"LUV HMMWV Jeep", "M 818", "M978 HEMTT Tanker", "Land_Rover_101_FC", "Land_Rover_109_S3"}, }, [veaf.ERA.COLD_WAR] = { [0] = {"Truck Land Rover 101 FC", "LUV Land Rover 109", "Truck M939 Heavy"}, [1] = {"Truck Land Rover 101 FC", "LUV Land Rover 109", "Truck M939 Heavy"}, [2] = {"Truck Land Rover 101 FC", "LUV Land Rover 109", "Truck M939 Heavy"}, [3] = {"Truck Land Rover 101 FC", "LUV Land Rover 109", "Truck M939 Heavy"}, [4] = {"Truck Land Rover 101 FC", "LUV Land Rover 109", "Truck M939 Heavy"}, [5] = {"Truck Land Rover 101 FC", "LUV Land Rover 109", "Truck M939 Heavy"}, }, [veaf.ERA.WW2] = { [0] = {"Bedford_MWD", "CCKW_353", "Willys_MB"}, [1] = {"Bedford_MWD", "CCKW_353", "Willys_MB"}, [2] = {"Bedford_MWD", "CCKW_353", "Willys_MB"}, [3] = {"Bedford_MWD", "CCKW_353", "Willys_MB"}, [4] = {"Bedford_MWD", "CCKW_353", "Willys_MB"}, [5] = {"Bedford_MWD", "CCKW_353", "Willys_MB"}, } }, [coalition.side.RED] = { [veaf.ERA.MODERN] = { [0]= {"ATZ-60_Maz", "ZIL-135", "ATZ-5", "Ural-4320 APA-5D", "SKP-11", "GAZ-66", "KAMAZ Truck", "Ural-375", "KrAZ6322", "ZIL-131 KUNG", "Tigr_233036", "UAZ-469"}, [1]= {"ATZ-60_Maz", "ZIL-135", "ATZ-5", "Ural-4320 APA-5D", "SKP-11", "GAZ-66", "KAMAZ Truck", "Ural-375", "KrAZ6322", "ZIL-131 KUNG", "Tigr_233036", "UAZ-469"}, [2]= {"ATZ-60_Maz", "ZIL-135", "ATZ-5", "Ural-4320 APA-5D", "SKP-11", "GAZ-66", "KAMAZ Truck", "Ural-375", "KrAZ6322", "ZIL-131 KUNG", "Tigr_233036", "UAZ-469"}, [3]= {"ATZ-60_Maz", "ZIL-135", "ATZ-5", "Ural-4320 APA-5D", "SKP-11", "GAZ-66", "KAMAZ Truck", "Ural-375", "KrAZ6322", "ZIL-131 KUNG", "Tigr_233036", "UAZ-469"}, [4]= {"ATZ-60_Maz", "ZIL-135", "ATZ-5", "Ural-4320 APA-5D", "SKP-11", "GAZ-66", "KAMAZ Truck", "Ural-375", "KrAZ6322", "ZIL-131 KUNG", "Tigr_233036", "UAZ-469"}, [5]= {"ATZ-60_Maz", "ZIL-135", "ATZ-5", "Ural-4320 APA-5D", "SKP-11", "GAZ-66", "KAMAZ Truck", "Ural-375", "KrAZ6322", "ZIL-131 KUNG", "Tigr_233036", "UAZ-469"}, }, [veaf.ERA.COLD_WAR] = { [0]= {"LUV UAZ-469 Jeep", "Refueler ATMZ-5", "Refueler ATZ-10", "Refueler ATZ-5", "S-75 Tractor (ZIL-131)", "Truck GAZ-66", "Truck KAMAZ 43101", "Truck Ural-4320", "Truck Ural-4320-31 Arm'd", "Truck Ural-4320T", "Truck ZIL-131 (C2)", "Truck ZIL-135"}, [1]= {"LUV UAZ-469 Jeep", "Refueler ATMZ-5", "Refueler ATZ-10", "Refueler ATZ-5", "S-75 Tractor (ZIL-131)", "Truck GAZ-66", "Truck KAMAZ 43101", "Truck Ural-4320", "Truck Ural-4320-31 Arm'd", "Truck Ural-4320T", "Truck ZIL-131 (C2)", "Truck ZIL-135"}, [2]= {"LUV UAZ-469 Jeep", "Refueler ATMZ-5", "Refueler ATZ-10", "Refueler ATZ-5", "S-75 Tractor (ZIL-131)", "Truck GAZ-66", "Truck KAMAZ 43101", "Truck Ural-4320", "Truck Ural-4320-31 Arm'd", "Truck Ural-4320T", "Truck ZIL-131 (C2)", "Truck ZIL-135"}, [3]= {"LUV UAZ-469 Jeep", "Refueler ATMZ-5", "Refueler ATZ-10", "Refueler ATZ-5", "S-75 Tractor (ZIL-131)", "Truck GAZ-66", "Truck KAMAZ 43101", "Truck Ural-4320", "Truck Ural-4320-31 Arm'd", "Truck Ural-4320T", "Truck ZIL-131 (C2)", "Truck ZIL-135"}, [4]= {"LUV UAZ-469 Jeep", "Refueler ATMZ-5", "Refueler ATZ-10", "Refueler ATZ-5", "S-75 Tractor (ZIL-131)", "Truck GAZ-66", "Truck KAMAZ 43101", "Truck Ural-4320", "Truck Ural-4320-31 Arm'd", "Truck Ural-4320T", "Truck ZIL-131 (C2)", "Truck ZIL-135"}, [5]= {"LUV UAZ-469 Jeep", "Refueler ATMZ-5", "Refueler ATZ-10", "Refueler ATZ-5", "S-75 Tractor (ZIL-131)", "Truck GAZ-66", "Truck KAMAZ 43101", "Truck Ural-4320", "Truck Ural-4320-31 Arm'd", "Truck Ural-4320T", "Truck ZIL-131 (C2)", "Truck ZIL-135"}, }, [veaf.ERA.WW2] = { [0] = {"Blitz_36-6700A", "Horch_901_typ_40_kfz_21", "Kubelwagen_82", "Sd_Kfz_7", "Sd_Kfz_2" }, [1] = {"Blitz_36-6700A", "Horch_901_typ_40_kfz_21", "Kubelwagen_82", "Sd_Kfz_7", "Sd_Kfz_2" }, [2] = {"Blitz_36-6700A", "Horch_901_typ_40_kfz_21", "Kubelwagen_82", "Sd_Kfz_7", "Sd_Kfz_2" }, [3] = {"Blitz_36-6700A", "Horch_901_typ_40_kfz_21", "Kubelwagen_82", "Sd_Kfz_7", "Sd_Kfz_2" }, [4] = {"Blitz_36-6700A", "Horch_901_typ_40_kfz_21", "Kubelwagen_82", "Sd_Kfz_7", "Sd_Kfz_2" }, [5] = {"Blitz_36-6700A", "Horch_901_typ_40_kfz_21", "Kubelwagen_82", "Sd_Kfz_7", "Sd_Kfz_2" }, } } } veafCasMission.ARMOR_TYPES = { [coalition.side.BLUE] = { [veaf.ERA.MODERN] = { [0] = {}, [1] = {"IFV Marder", "MCV-80", "IFV LAV-25", "M1134 Stryker ATGM", "M-2 Bradley"}, [2] = {"IFV Marder", "MCV-80", "IFV LAV-25", "M1134 Stryker ATGM", "M-2 Bradley"}, [3] = {"IFV Marder", "VAB_Mephisto", "M-2 Bradley", "MBT Leopard 1A3", "Chieftain_mk3"}, [4] = {"M-2 Bradley", "MBT Leopard 1A3", "Merkava_Mk4", "M1128 Stryker MGS"}, [5] = {"Merkava_Mk4", "Challenger2", "Leclerc", "Leopard-2", "M-1 Abrams"}, }, [veaf.ERA.COLD_WAR] = { [0] = {}, [1] = {"APC M113", "APC TPz Fuchs", "APC AAV-7 Amphibious"}, [2] = {"APC M113", "APC TPz Fuchs", "APC AAV-7 Amphibious", "IFV Marder"}, [3] = {"APC M113", "APC TPz Fuchs", "APC AAV-7 Amphibious", "IFV Marder", "MBT M60A3 Patton"}, [4] = {"APC AAV-7 Amphibious", "IFV Marder", "MBT M60A3 Patton", "MBT Leopard 1A3", "MBT Chieftain Mk.3"}, [5] = {"APC AAV-7 Amphibious", "IFV Marder", "MBT M60A3 Patton", "MBT Leopard 1A3", "MBT Chieftain Mk.3"}, }, [veaf.ERA.WW2] = { [0] = {}, [1] = {"M30_CC", "M10_GMC"}, [2] = {"M30_CC", "M10_GMC"}, [3] = {"M30_CC", "M10_GMC", "Centaur_IV"}, [4] = {"Centaur_IV", "Churchill_VII", "Cromwell_IV"}, [5] = {"Centaur_IV", "Churchill_VII", "Cromwell_IV", "M4_Sherman", "M4A4_Sherman_FF"} } }, [coalition.side.RED] = { [veaf.ERA.MODERN] = { [0] = {}, [1] = {"BTR-82A", "BMP-1", "VAB_Mephisto"}, [2] = {"BTR-82A", "BMP-1", "VAB_Mephisto", "BMP-2"}, [3] = {"BTR-82A", "VAB_Mephisto", "BMP-2", "T-55", "Chieftain_mk3"}, [4] = {"BTR-82A", "BMP-3", "Chieftain_mk3", "T-72B"}, [5] = {"BMP-3", "ZTZ96B", "T-72B3", "T-80UD", "T-90"} }, [veaf.ERA.COLD_WAR] = { [0] = {}, [1] = {"Scout BRDM-2", "APC MTLB"}, [2] = {"Scout BRDM-2", "APC MTLB", "IFV BMD-1", "IFV BMP-1"}, [3] = {"Scout BRDM-2", "APC MTLB", "IFV BMD-1", "IFV BMP-1", "APC BTR-RD"}, [4] = {"APC MTLB", "IFV BMD-1", "IFV BMP-1", "APC BTR-RD", "LT PT-76"}, [5] = {"IFV BMD-1", "IFV BMP-1", "APC BTR-RD", "LT PT-76", "MBT T-55"}, }, [veaf.ERA.WW2] = { [0] = {}, [1] = {"Sd_Kfz_251", "Sd_Kfz_234_2_Puma"}, [2] = {"Sd_Kfz_251", "Sd_Kfz_234_2_Puma"}, [3] = {"Sd_Kfz_251", "Sd_Kfz_234_2_Puma", "Elefant_SdKfz_184"}, [4] = {"Pz_IV_H", "Tiger_I", "Tiger_II_H", "Stug_III", "Stug_IV"}, [5] = {"Pz_IV_H", "Tiger_I", "Tiger_II_H", "Stug_III", "Stug_IV", "JagdPz_IV", "Jagdpanther_G1", "Pz_V_Panther_G"} } } } veafCasMission.INFANTRY_TYPES = { [coalition.side.BLUE] = { [veaf.ERA.MODERN] = {"Soldier RPG", "Soldier M249", "Soldier M4 GRG"}, [veaf.ERA.COLD_WAR] = {"Soldier RPG", "Soldier M249", "Soldier M4 GRG"}, [veaf.ERA.WW2] = {"Soldier RPG", "Soldier M249", "Soldier M4 GRG"}, }, [coalition.side.RED] = { [veaf.ERA.MODERN] = {"Paratrooper RPG-16", "Infantry AK ver3", "Infantry AK ver2"}, [veaf.ERA.COLD_WAR] = {"Paratrooper RPG-16", "Infantry AK ver3", "Infantry AK ver2"}, [veaf.ERA.WW2] = {"Paratrooper RPG-16", "Infantry AK ver3", "Infantry AK ver2"}, } } veafCasMission.INFANTRY_IFV_TYPES = { [coalition.side.BLUE] = { [veaf.ERA.MODERN] = { [0] = {"Land_Rover_101_FC", "Land_Rover_109_S3"}, [1] = {"IFV Marder"}, [2] = {"IFV Marder"}, [3] = {"M-2 Bradley"}, [4] = {"M-2 Bradley"}, [5] = {"M-2 Bradley"}, }, [veaf.ERA.COLD_WAR] = { [0] = {"Truck M939 Heavy"}, [1] = {"APC M113", "APC TPz Fuchs", "APC AAV-7 Amphibious"}, [2] = {"APC M113", "APC TPz Fuchs", "APC AAV-7 Amphibious", "IFV Marder"}, [3] = {"APC M113", "APC TPz Fuchs", "APC AAV-7 Amphibious", "IFV Marder"}, [4] = {"APC AAV-7 Amphibious", "IFV Marder"}, [5] = {"APC AAV-7 Amphibious", "IFV Marder"}, }, [veaf.ERA.WW2] = { [0] = {"Bedford_MWD"}, [1] = {"APC M2A1 Halftrack"}, [2] = {"APC M2A1 Halftrack"}, [3] = {"M-2 Bradley"}, [4] = {"M-2 Bradley"}, [5] = {"M-2 Bradley"}, }, }, [coalition.side.RED] = { [veaf.ERA.MODERN] = { [0] = {"Ural-4320 APA-5D", "GAZ-66", "KAMAZ Truck"}, [1] = {"BMP-1"}, [2] = {"BMP-1"}, [3] = {"BMP-2"}, [4] = {"BMP-2"}, [5] = {"BMP-2"}, }, [veaf.ERA.COLD_WAR] = { [0] = {"Truck KAMAZ 43101", "Truck ZIL-135", "Truck Ural-4320-31 Arm'd"}, [1] = {"APC MTLB"}, [2] = {"APC MTLB", "APC BTR-RD"}, [3] = {"APC MTLB", "APC BTR-RD", "IFV BMD-1"}, [4] = {"APC MTLB", "APC BTR-RD", "IFV BMD-1", "IFV BMP-1"}, [5] = {"APC BTR-RD", "IFV BMD-1", "IFV BMP-1"}, }, [veaf.ERA.WW2] = { [0] = {"Blitz_36-6700A", "Horch_901_typ_40_kfz_21", "Kubelwagen_82", "Sd_Kfz_7", "Sd_Kfz_2"}, [1] = {"Sd_Kfz_251"}, [2] = {"Sd_Kfz_251"}, [3] = {"Sd_Kfz_234_2_Puma"}, [4] = {"Sd_Kfz_234_2_Puma"}, [5] = {"Sd_Kfz_234_2_Puma"}, } } } ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Radio menus paths veafCasMission.targetMarkersPath = nil veafCasMission.targetInfoPath = nil veafCasMission.rootPath = nil -- CAS Group watchdog function id veafCasMission.groupAliveCheckTaskID = 'none' -- Smoke reset function id veafCasMission.smokeResetTaskID = 'none' -- Flare reset function id veafCasMission.flareResetTaskID = 'none' veafCasMission.SIDE_RED = coalition.side.RED veafCasMission.SIDE_BLUE = coalition.side.BLUE ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function executed when a mark has changed. This happens when text is entered or changed. function veafCasMission.onEventMarkChange(eventPos, event) veaf.loggers.get(veafCasMission.Id):trace(string.format("event = %s", veaf.p(event))) -- choose by default the coalition opposing the player who triggered the event local invertedCoalition = 1 if event.coalition == 1 then invertedCoalition = 2 end veaf.loggers.get(veafCasMission.Id):trace(string.format("event.idx = %s", veaf.p(event.idx))) if veafCasMission.executeCommand(eventPos, event.text, invertedCoalition, event.idx) then -- Delete old mark. veaf.loggers.get(veafCasMission.Id):trace(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafCasMission.executeCommand(eventPos, eventText, coalition, markId, bypassSecurity) veaf.loggers.get(veafCasMission.Id):debug(string.format("veafCasMission.executeCommand(eventText=[%s])", eventText)) veaf.loggers.get(veafCasMission.Id):trace(string.format("coalition=%s", veaf.p(coalition))) veaf.loggers.get(veafCasMission.Id):trace(string.format("markId=%s", veaf.p(markId))) veaf.loggers.get(veafCasMission.Id):trace(string.format("bypassSecurity=%s", veaf.p(bypassSecurity))) -- Check if marker has a text and the veafCasMission.keyphrase keyphrase. if eventText ~= nil and eventText:lower():find(veafCasMission.Keyphrase) then -- Analyse the mark point text and extract the keywords. local options = veafCasMission.markTextAnalysis(eventText) if options then -- Check options commands if options.casmission then if not (bypassSecurity or veafSecurity.checkSecurity_L9(options.password, markId)) then return end if not options.side then if options.country then -- deduct the side from the country options.side = veaf.getCoalitionForCountry(options.country, true) else options.side = coalition end end if not options.country then -- deduct the country from the side options.country = veaf.getCountryForCoalition(options.side) end -- create the group veafCasMission.generateCasMission(eventPos, options.size, options.defense, options.armor, options.spacing, options.disperseOnAttack, options.side) return true end end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Analyse the mark text and extract keywords. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Extract keywords from mark text. function veafCasMission.markTextAnalysis(text) -- Option parameters extracted from the mark text. local switch = {} switch.casmission = false -- size ; ranges from 1 to 5, 5 being the biggest. switch.size = 1 -- defenses force ; ranges from 1 to 5, 5 being the toughest. switch.defense = 1 -- armor force ; ranges from 1 to 5, 5 being the strongest and most modern. switch.armor = 1 -- spacing ; ranges from 1 to 5, 1 being the default and 5 being the widest spacing. switch.spacing = 1 -- disperse on attack ; self explanatory, if keyword is present the option will be set to true switch.disperseOnAttack = false -- password switch.password = nil -- coalition switch.side = nil -- Check for correct keywords. if text:lower():find(veafCasMission.Keyphrase) then switch.casmission = true else return nil end -- keywords are split by "," local keywords = veaf.split(text, ",") for _, keyphrase in pairs(keywords) do -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. local str = veaf.breakString(veaf.trim(keyphrase), " ") local key = str[1] local val = str[2] if key:lower() == "password" then -- Unlock the command veaf.loggers.get(veafCasMission.Id):debug(string.format("Keyword password", val)) switch.password = val end if switch.casmission and key:lower() == "size" then -- Set size. veaf.loggers.get(veafCasMission.Id):debug(string.format("Keyword size = %d", val)) local nVal = tonumber(val) if nVal <= 5 and nVal >= 1 then switch.size = nVal end end if switch.casmission and key:lower() == "defense" then -- Set defense. veaf.loggers.get(veafCasMission.Id):debug(string.format("Keyword defense = %d", val)) local nVal = tonumber(val) if nVal <= 5 and nVal >= 0 then switch.defense = nVal end end if switch.casmission and key:lower() == "armor" then -- Set armor. veaf.loggers.get(veafCasMission.Id):debug(string.format("Keyword armor = %d", val)) local nVal = tonumber(val) if nVal <= 5 and nVal >= 0 then switch.armor = nVal end end if switch.casmission and key:lower() == "spacing" then -- Set spacing. veaf.loggers.get(veafCasMission.Id):debug(string.format("Keyword spacing = %d", val)) local nVal = tonumber(val) if nVal <= 5 and nVal >= 1 then switch.spacing = nVal end end if key:lower() == "side" then -- Set side veaf.loggers.get(veafCasMission.Id):trace(string.format("Keyword side = %s", val)) if val:upper() == "BLUE" then switch.side = veafCasMission.SIDE_BLUE else switch.side = veafCasMission.SIDE_RED end end if switch.casmission and key:lower() == "disperse" then -- Set disperse on attack. veaf.loggers.get(veafCasMission.Id):debug("Keyword disperse = %s", val) if val ~= "" then local nVal = tonumber(val) if nVal then switch.disperseOnAttack = nVal end else switch.disperseOnAttack = 15 end end end return switch end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- CAS target group generation and management ------------------------------------------------------------------------------------------------------------------------------------------------------------- local function _addDefenseForGroups(group, side, defense, multiple, forInfantry) veaf.loggers.get(veafCasMission.Id):trace(string.format("_addDefenseForGroups(defense=[%s], side=[%s], multiple=[%s], forInfantry=[%s])", veaf.p(defense), veaf.p(side), veaf.p(multiple), veaf.p(forInfantry))) local _actualDefense = defense if defense > 0 then -- roll a dice : 20% chance to get a -1 (lower) difficulty, 30% chance to get a +1 (higher) difficulty, and 50% to get what was asked for local _dice = math.random(100) veaf.loggers.get(veafCasMission.Id):trace("_dice = " .. _dice) if _dice <= 20 then _actualDefense = defense - 1 elseif _dice > 80 then _actualDefense = defense + 1 end end if _actualDefense > 5 then _actualDefense = 6 end if _actualDefense < 0 then _actualDefense = 0 end veaf.loggers.get(veafCasMission.Id):trace("_actualDefense = " .. _actualDefense) for _ = 1, multiple do if _actualDefense > 5 then if side == veafCasMission.SIDE_BLUE then if forInfantry then -- only spawn manpads for _ = 1, math.random(1,_actualDefense-2) do table.insert(group.units, { "Stinger comm", random=true }) table.insert(group.units, { "Soldier stinger", random=true }) end else table.insert(group.units, { "M1097 Avenger", random=true }) table.insert(group.units, { "Roland ADS", random=true }) table.insert(group.units, { "Gepard", random=true }) end else if forInfantry then -- only spawn manpads for _ = 1, math.random(1,_actualDefense-2) do -- for _actualDefense = 4-5, spawn a modern Igla-S team table.insert(group.units, { "SA-18 Igla-S comm", random=true }) table.insert(group.units, { "SA-18 Igla-S manpad", random=true }) end else table.insert(group.units, { veaf.randomlyChooseFrom({"2S6 Tunguska", "Tor 9A331", "Tor 9A331"}), random=true }) table.insert(group.units, { "Strela-10M3", random=true }) table.insert(group.units, { "ZSU-23-4 Shilka", random=true }) end end elseif _actualDefense == 5 then if side == veafCasMission.SIDE_BLUE then if forInfantry then -- only spawn manpads for _ = 1, math.random(1,_actualDefense-2) do table.insert(group.units, { "Stinger comm", random=true }) table.insert(group.units, { "Soldier stinger", random=true }) end else table.insert(group.units, { veaf.randomlyChooseFrom({"Gepard", "M1097 Avenger", "M1097 Avenger"}), random=true }) table.insert(group.units, { "Roland ADS", random=true }) end else if forInfantry then -- only spawn manpads for _ = 1, math.random(1,_actualDefense-2) do -- for _actualDefense = 4-5, spawn a modern Igla-S team table.insert(group.units, { "SA-18 Igla-S comm", random=true }) table.insert(group.units, { "SA-18 Igla-S manpad", random=true }) end else table.insert(group.units, { veaf.randomlyChooseFrom({"Osa 9A33 ln", "2S6 Tunguska"}), random=true }) table.insert(group.units, { veaf.randomlyChooseFrom({"ZSU-23-4 Shilka", "Strela-10M3"}), random=true }) end end elseif _actualDefense == 4 then if side == veafCasMission.SIDE_BLUE then if forInfantry then -- only spawn manpads for _ = 1, math.random(1,_actualDefense-2) do table.insert(group.units, { "Stinger comm", random=true }) table.insert(group.units, { "Soldier stinger", random=true }) end else table.insert(group.units, { "Gepard", random=true }) table.insert(group.units, { "Roland ADS", random=true }) end else if forInfantry then -- only spawn manpads for _ = 1, math.random(1,_actualDefense-2) do -- for _actualDefense = 4-5, spawn a modern Igla-S team table.insert(group.units, { "SA-18 Igla-S comm", random=true }) table.insert(group.units, { "SA-18 Igla-S manpad", random=true }) end else table.insert(group.units, { veaf.randomlyChooseFrom({"ZSU-23-4 Shilka", "ZSU-23-4 Shilka", "ZSU_57_2"}), random=true }) table.insert(group.units, { veaf.randomlyChooseFrom({"HQ-7_LN_EO", "HQ-7_LN_SP"}), random=true }) end end elseif _actualDefense == 3 then if side == veafCasMission.SIDE_BLUE then if forInfantry then -- only spawn manpads for _ = 1, math.random(1,_actualDefense-2) do table.insert(group.units, { "Stinger comm", random=true }) table.insert(group.units, { "Soldier stinger", random=true }) end else table.insert(group.units, { veaf.randomlyChooseFrom({"M48 Chaparral", "M6 Linebacker"}), random=true }) table.insert(group.units, { "Gepard", random=true }) end else if forInfantry then -- only spawn manpads for _ = 1, math.random(1,_actualDefense-2) do -- for _actualDefense = 3, spawn an older Igla team table.insert(group.units, { "SA-18 Igla comm", random=true }) table.insert(group.units, { "SA-18 Igla manpad", random=true }) end else table.insert(group.units, { veaf.randomlyChooseFrom({"Strela-1 9P31", "Strela-10M3"}), random=true }) table.insert(group.units, { veaf.randomlyChooseFrom({"ZSU-23-4 Shilka", "ZSU-23-4 Shilka", "ZSU_57_2"}), random=true }) end end elseif _actualDefense == 2 then if side == veafCasMission.SIDE_BLUE then table.insert(group.units, { "Gepard", random=true }) table.insert(group.units, { "Vulcan", random=true }) else table.insert(group.units, { veaf.randomlyChooseFrom({"ZSU-23-4 Shilka", "ZSU_57_2"}), random=true }) table.insert(group.units, { veaf.randomlyChooseFrom({"ZSU-23-4 Shilka", "ZSU_57_2"}), random=true }) end elseif _actualDefense == 1 then if side == veafCasMission.SIDE_BLUE then table.insert(group.units, { "Vulcan", random=true }) else table.insert(group.units, { veaf.randomlyChooseFrom({"Ural-375 ZU-23", "ZSU_57_2"}), random=true }) end end end --veaf.loggers.get(veafCasMission.Id):trace(string.format("group.units=%s", veaf.p(group.units))) end --- TODO/feat-era/ Generates an air defense group function veafCasMission.generateAirDefenseGroup(groupName, defense, side) side = side or veafCasMission.SIDE_RED -- generate a primary air defense platoon local _actualDefense = defense if defense > 0 then -- roll a dice : 20% chance to get a -1 (lower) difficulty, 30% chance to get a +1 (higher) difficulty, and 50% to get what was asked for local _dice = math.random(100) veaf.loggers.get(veafCasMission.Id):trace("_dice = " .. _dice) if _dice <= 20 then _actualDefense = defense - 1 elseif _dice > 80 then _actualDefense = defense + 1 end end if _actualDefense > 5 then _actualDefense = 5 end if _actualDefense < 0 then _actualDefense = 0 end veaf.loggers.get(veafCasMission.Id):trace("_actualDefense = " .. _actualDefense) local _groupDefinition = "generateAirDefenseGroup-BLUE-" if side == veafCasMission.SIDE_RED then _groupDefinition = "generateAirDefenseGroup-RED-" end _groupDefinition = _groupDefinition .. tostring(_actualDefense) veaf.loggers.get(veafCasMission.Id):trace("_groupDefinition = " .. _groupDefinition) local group = veafUnits.findGroup(_groupDefinition) if not group then veaf.loggers.get(veafCasMission.Id):error(string.format("veafCasMission.generateAirDefenseGroup cannot find group [%s]", _groupDefinition or "")) end group.description = groupName group.groupName = groupName veaf.loggers.get(veafCasMission.Id):trace("#group.units = " .. #group.units) return group end --- Generates a transport company and its air defenses function veafCasMission.generateTransportCompany(groupName, defense, side, size) veaf.loggers.get(veafCasMission.Id):trace(string.format("veafCasMission.generateTransportCompany(groupName=[%s], defense=[%s], side=[%s], size=[%s])", groupName or "", defense or "", side or "", size or "")) side = side or veafCasMission.SIDE_RED local groupCount = math.floor((size or math.random(10, 15))) veaf.loggers.get(veafCasMission.Id):trace(string.format("groupCount=%s", tostring(groupCount))) local group = { disposition = { h = groupCount, w = groupCount}, units = {}, description = groupName, groupName = groupName, } -- generate a transport company local chooseFrom = veafCasMission.TRANSPORT_TYPES[side][veaf.config.era][0] veaf.loggers.get(veafCasMission.Id):trace("chooseFrom=%s", chooseFrom) for _ = 1, groupCount do local transportType = veaf.randomlyChooseFrom(chooseFrom) table.insert(group.units, { transportType, random=true}) end -- TODO/feat-era/ add an air defense vehicle every 10 vehicles local nbDefense = groupCount / 10 + 1 if nbDefense == 0 then nbDefense = 1 end veaf.loggers.get(veafCasMission.Id):debug("nbDefense = " .. nbDefense) if not veaf.config.ww2 then _addDefenseForGroups(group, side, defense, nbDefense) else -- nothing, there are no mobile defense units in WW2 end return group end --- Generates an armor platoon and its air defenses function veafCasMission.generateArmorPlatoon(groupName, defense, armor, side, size) veaf.loggers.get(veafCasMission.Id):trace(string.format("veafCasMission.generateArmorPlatoon(groupName=[%s], defense=[%s], armor=[%s], side=[%s], size=[%s])", groupName or "", defense or "", armor or "", side or "", size or "")) side = side or veafCasMission.SIDE_RED -- generate an armor platoon local groupCount = math.floor((size or math.random(3, 6)) * (math.random(8, 12)/10)) veaf.loggers.get(veafCasMission.Id):trace(string.format("groupCount=%s", tostring(groupCount))) local group = { disposition = { h = groupCount, w = groupCount}, units = {}, description = groupName, groupName = groupName, } if group.disposition.h < 4 then group.disposition.h = 4 group.disposition.w = 4 end local armorBias = 0 if armor < 0 then armor = 0 end if armor > 5 then armorBias = armor - 5 armor = 5 end local chooseFrom = veafCasMission.ARMOR_TYPES[side][veaf.config.era][armor] veaf.loggers.get(veafCasMission.Id):trace("chooseFrom=%s", chooseFrom) for _ = 1, groupCount do local armorType = veaf.randomlyChooseFrom(chooseFrom, armorBias) if armorType then table.insert(group.units, { armorType, random=true }) end end -- TODO/feat-era/ add air defense vehicles if not veaf.config.ww2 then _addDefenseForGroups(group, side, defense, 1) else -- nothing, there are no mobile defense units in WW2 end return group end --- Generates an infantry group along with its manpad units and tranport vehicles function veafCasMission.generateInfantryGroup(groupName, defense, armor, side, size) side = side or veafCasMission.SIDE_RED veaf.loggers.get(veafCasMission.Id):trace(string.format("veafCasMission.generateInfantryGroup(groupName=%s, defense=%d, armor=%d)",groupName, defense, armor)) -- generate an infantry group local groupCount = math.floor((size or math.random(3, 6)) * (math.random(8, 12)/10)) veaf.loggers.get(veafCasMission.Id):trace(string.format("groupCount=%s", tostring(groupCount))) local group = { disposition = { h = groupCount, w = groupCount}, units = {}, description = groupName, groupName = groupName, } if group.disposition.h < 4 then group.disposition.h = 4 group.disposition.w = 4 end local chooseFrom = veafCasMission.INFANTRY_TYPES[side][veaf.config.era] veaf.loggers.get(veafCasMission.Id):trace("chooseFrom=%s", chooseFrom) for _ = 1, groupCount do local unitType = veaf.randomlyChooseFrom(chooseFrom) table.insert(group.units, { unitType }) end -- add a transport vehicle or an APC/IFV depending on the side and the era chooseFrom = veafCasMission.INFANTRY_IFV_TYPES[side][veaf.config.era][armor] veaf.loggers.get(veafCasMission.Id):trace("chooseFrom=%s", chooseFrom) local unitType = veaf.randomlyChooseFrom(chooseFrom) table.insert(group.units, { unitType, cell=11, random=true }) -- TODO/feat-era/ add air defense if not veaf.config.ww2 then _addDefenseForGroups(group, side, defense, 1, true) else -- nothing, there are no mobile defense units in WW2 end return group end function veafCasMission.placeGroup(groupDefinition, spawnPosition, spacing, resultTable, hasDest) if spawnPosition ~= nil and groupDefinition ~= nil then veaf.loggers.get(veafCasMission.Id):trace(string.format("veafCasMission.placeGroup(#groupDefinition.units=%d)",#groupDefinition.units)) -- process the group veaf.loggers.get(veafCasMission.Id):trace("process the group") local group = veafUnits.processGroup(groupDefinition) -- place its units local groupPosition = { x = spawnPosition.x, z = spawnPosition.y } local hdg = math.random(359) local group, cells = veafUnits.placeGroup(group, veaf.placePointOnLand(groupPosition), spacing+3, hdg, hasDest) if veaf.Trace then veafUnits.traceGroup(group, cells) end -- add the units to the result units list if not resultTable then resultTable = {} end for _,u in pairs(group.units) do table.insert(resultTable, u) end end veaf.loggers.get(veafCasMission.Id):trace(string.format("#resultTable=%d",#resultTable)) return resultTable end --- Generates a complete CAS target group function veafCasMission.generateCasGroup(casGroupName, spawnSpot, size, defense, armor, spacing, side) veaf.loggers.get(veafCasMission.Id):trace("side = " .. tostring(side)) side = side or veafCasMission.SIDE_RED local units = {} local zoneRadius = (size+spacing)*350 veaf.loggers.get(veafCasMission.Id):trace("zoneRadius = " .. zoneRadius) -- generate between size-2 and size+1 infantry groups local infantryGroupsCount = math.random(math.max(1, size-2), size + 1) veaf.loggers.get(veafCasMission.Id):trace("infantryGroupsCount = " .. infantryGroupsCount) for infantryGroupNumber = 1, infantryGroupsCount do local groupName = casGroupName .. " - Infantry Section " .. infantryGroupNumber local groupPosition = veaf.findPointInZone(spawnSpot, zoneRadius, false) veaf.loggers.get(veafCasMission.Id):trace(string.format("infantry group #%s position : %s", veaf.p(infantryGroupNumber), veaf.p(groupPosition))) local group = veafCasMission.generateInfantryGroup(groupName, defense, armor, side) veafCasMission.placeGroup(group, groupPosition, spacing, units) end if armor > 0 then -- generate between size-2 and size+1 armor platoons local armorPlatoonsCount = math.random(math.max(1, size-2), size + 1) veaf.loggers.get(veafCasMission.Id):trace("armorPlatoonsCount = " .. armorPlatoonsCount) for armorGroupNumber = 1, armorPlatoonsCount do local groupName = casGroupName .. " - Armor Platoon " .. armorGroupNumber local groupPosition = veaf.findPointInZone(spawnSpot, zoneRadius, false) veaf.loggers.get(veafCasMission.Id):trace(string.format("armor group #%s position : %s", veaf.p(armorGroupNumber), veaf.p(groupPosition))) local group = veafCasMission.generateArmorPlatoon(groupName, defense, armor, side) veafCasMission.placeGroup(group, groupPosition, spacing, units) end end if defense > 0 then -- generate between 1 and 2 air defense groups local airDefenseGroupsCount = 1 if defense > 3 then airDefenseGroupsCount = 2 end veaf.loggers.get(veafCasMission.Id):trace("airDefenseGroupsCount = " .. airDefenseGroupsCount) for airDefenseGroupNumber = 1, airDefenseGroupsCount do local groupName = casGroupName .. " - Air Defense Group ".. airDefenseGroupNumber local groupPosition = veaf.findPointInZone(spawnSpot, zoneRadius, false) veaf.loggers.get(veafCasMission.Id):trace(string.format("air defense group #%s position : %s", veaf.p(airDefenseGroupNumber), veaf.p(groupPosition))) local group = veafCasMission.generateAirDefenseGroup(groupName, defense, side) veafCasMission.placeGroup(group, groupPosition, spacing, units) end end -- generate between 1 and size transport companies local transportCompaniesCount = math.random(1, size) veaf.loggers.get(veafCasMission.Id):trace("transportCompaniesCount = " .. transportCompaniesCount) for transportCompanyGroupNumber = 1, transportCompaniesCount do local groupName = casGroupName .. " - Transport Company " .. transportCompanyGroupNumber local groupPosition = veaf.findPointInZone(spawnSpot, zoneRadius, false) veaf.loggers.get(veafCasMission.Id):trace(string.format("transport group #%s position : %s", veaf.p(transportCompanyGroupNumber), veaf.p(groupPosition))) local group = veafCasMission.generateTransportCompany(groupName, defense, side) veafCasMission.placeGroup(group, groupPosition, spacing, units) end return units end --- Generates a CAS mission function veafCasMission.generateCasMission(spawnSpot, size, defense, armor, spacing, disperseOnAttack, side) if veafCasMission.groupAliveCheckTaskID ~= 'none' then trigger.action.outText("A CAS target group already exists !", 15) return end if side == veafCasMission.SIDE_BLUE then veafCasMission.casGroupName = veafCasMission.BlueCasGroupName end local country = veaf.getCountryForCoalition(side) local units = veafCasMission.generateCasGroup(veafCasMission.casGroupName, spawnSpot, size, defense, armor, spacing, side) -- prepare the actual DCS units local dcsUnits = {} for i=1, #units do local unit = units[i] local unitType = unit.typeName local unitName = veafCasMission.casGroupName .. " / " .. unit.displayName .. " #" .. i local unitHdg = unit.hdg local spawnPosition = unit.spawnPoint -- check if position is correct for the unit type if veafUnits.checkPositionForUnit(spawnPosition, unit) then local toInsert = { ["x"] = spawnPosition.x, ["y"] = spawnPosition.z, ["alt"] = spawnPosition.y, ["type"] = unitType, ["name"] = unitName, ["speed"] = 0, ["skill"] = "Random", ["heading"] = unitHdg, } table.insert(dcsUnits, toInsert) end end -- actually spawn groups mist.dynAdd({country = country, category = "GROUND_UNIT", name = veafCasMission.casGroupName, hidden = false, units = dcsUnits}) -- set AI options local controller = Group.getByName(veafCasMission.casGroupName):getController() controller:setOption(9, 2) -- set alarm state to red controller:setOption(AI.Option.Ground.id.DISPERSE_ON_ATTACK, disperseOnAttack) -- set disperse on attack according to the option -- Spawn Reaper local opposing_side = coalition.side.BLUE if coalition.side.RED ~= side then opposing_side = coalition.side.RED end local avgPos = veaf.getAveragePosition(veafCasMission.casGroupName) veafCasMission.afacName = veafSpawn.spawnAFAC(avgPos, "mq9", veaf.getCountryForCoalition(opposing_side), nil, nil, nil, veafSpawn.convertLaserToFreq(1688), "FM", 1688, true, false, false) -- build menu for each player veafRadio.addCommandToSubmenu('Target information', veafCasMission.rootPath, veafCasMission.reportTargetInformation, nil, veafRadio.USAGE_ForGroup) -- add radio menus for commands veafRadio.addSecuredCommandToSubmenu('Skip current objective', veafCasMission.rootPath, veafCasMission.skipCasTarget) veafCasMission.targetMarkersPath = veafRadio.addSubMenu("Target markers", veafCasMission.rootPath) veafRadio.addCommandToSubmenu('Request smoke on target area', veafCasMission.targetMarkersPath, veafCasMission.smokeCasTargetGroup) veafRadio.addCommandToSubmenu('Request illumination flare over target area', veafCasMission.targetMarkersPath, veafCasMission.flareCasTargetGroup) local nbVehicles, nbInfantry = veafUnits.countInfantryAndVehicles(veafCasMission.casGroupName) local message = "TARGET: Group of " .. nbVehicles .. " vehicles and " .. nbInfantry .. " soldiers. See F10 radio menu for details\n" trigger.action.outText(message,5) veafRadio.refreshRadioMenu() -- start checking for targets destruction veafCasMission.casGroupWatchdog() end -- Ask a report -- @param int groupId function veafCasMission.reportTargetInformation(unitName) -- generate information dispatch local nbVehicles, nbInfantry = veafUnits.countInfantryAndVehicles(veafCasMission.casGroupName) local message = "TARGET: Group of " .. nbVehicles .. " vehicles and " .. nbInfantry .. " soldiers.\n" if veafCasMission.afacName then message = message .. "AFAC on station: " .. veafCasMission.afacName .. "\n" end message = message .. "\n" -- add coordinates and position from bullseye local averageGroupPosition = veaf.getAveragePosition(veafCasMission.casGroupName) local lat, lon = coord.LOtoLL(averageGroupPosition) local mgrsString = mist.tostringMGRS(coord.LLtoMGRS(lat, lon), 3) local bullseye = mist.utils.makeVec3(mist.DBs.missionData.bullseye.blue, 0) local vec = {x = averageGroupPosition.x - bullseye.x, y = averageGroupPosition.y - bullseye.y, z = averageGroupPosition.z - bullseye.z} local dir = mist.utils.round(mist.utils.toDegree(mist.utils.getDir(vec, bullseye)), 0) local dist = mist.utils.get2DDist(averageGroupPosition, bullseye) local distMetric = mist.utils.round(dist/1000, 0) local distImperial = mist.utils.round(mist.utils.metersToNM(dist), 0) local fromBullseye = string.format('%03d', dir) .. ' for ' .. distMetric .. 'km /' .. distImperial .. 'nm' message = message .. "LAT LON (decimal): " .. mist.tostringLL(lat, lon, 2) .. ".\n" message = message .. "LAT LON (DMS) : " .. mist.tostringLL(lat, lon, 0, true) .. ".\n" message = message .. "MGRS/UTM : " .. mgrsString .. ".\n" message = message .. "FROM BULLSEYE : " .. fromBullseye .. ".\n" message = message .. "\n" message = message .. veaf.weatherReport(averageGroupPosition, nil, true) -- send message only for the unit veaf.outTextForGroup(unitName, message, 30) end --- add a smoke marker over the target area function veafCasMission.smokeCasTargetGroup() veaf.loggers.get(veafCasMission.Id):trace("veafCasMission.smokeCasTargetGroup START") veafSpawn.spawnSmoke(veaf.getAveragePosition(veafCasMission.casGroupName), trigger.smokeColor.Red) trigger.action.outText('Copy smoke requested, RED smoke on the deck!',5) veafRadio.delCommand(veafCasMission.targetMarkersPath, 'Request smoke on target area') veafRadio.addCommandToSubmenu('Target is marked with red smoke', veafCasMission.targetMarkersPath, veaf.emptyFunction) veafCasMission.smokeResetTaskID = mist.scheduleFunction(veafCasMission.smokeReset,{},timer.getTime()+veafCasMission.SecondsBetweenSmokeRequests) veafRadio.refreshRadioMenu() end --- Reset the smoke request radio menu function veafCasMission.smokeReset() veafRadio.delCommand(veafCasMission.targetMarkersPath, 'Target is marked with red smoke') veafRadio.addCommandToSubmenu('Request smoke on target area', veafCasMission.targetMarkersPath, veafCasMission.smokeCasTargetGroup) trigger.action.outText('Smoke marker available',5) veafRadio.refreshRadioMenu() end --- add an illumination flare over the target area function veafCasMission.flareCasTargetGroup() veafSpawn.spawnIlluminationFlare(veaf.getAveragePosition(veafCasMission.casGroupName)) trigger.action.outText('Copy illumination flare requested, illumination flare over target area!',5) veafRadio.delCommand(veafCasMission.targetMarkersPath, 'Request illumination flare over target area') veafRadio.addCommandToSubmenu('Target area is marked with illumination flare', veafCasMission.targetMarkersPath, veaf.emptyFunction) veafCasMission.flareResetTaskID = mist.scheduleFunction(veafCasMission.flareReset,{},timer.getTime()+veafCasMission.SecondsBetweenFlareRequests) veafRadio.refreshRadioMenu() end --- Reset the flare request radio menu function veafCasMission.flareReset() veafRadio.delCommand(veafCasMission.targetMarkersPath, 'Target area is marked with illumination flare') veafRadio.addCommandToSubmenu('Request illumination flare over target area', veafCasMission.targetMarkersPath, veafCasMission.flareCasTargetGroup) trigger.action.outText('Target illumination available',5) veafRadio.refreshRadioMenu() end --- Checks if the vehicles group is still alive, and if not announces the end of the CAS mission function veafCasMission.casGroupWatchdog() local nbVehicles, nbInfantry = veafUnits.countInfantryAndVehicles(veafCasMission.casGroupName) if nbVehicles > 0 then veaf.loggers.get(veafCasMission.Id):trace("Group is still alive with "..nbVehicles.." vehicles and "..nbInfantry.." soldiers") veafCasMission.groupAliveCheckTaskID = mist.scheduleFunction(veafCasMission.casGroupWatchdog,{},timer.getTime()+veafCasMission.SecondsBetweenWatchdogChecks) else trigger.action.outText("CAS objective group destroyed!", 5) veafCasMission.cleanupAfterMission() end end --- Called from the "Skip target" radio menu : remove the current CAS target group function veafCasMission.skipCasTarget() veafCasMission.cleanupAfterMission() trigger.action.outText("CAS objective group cleaned up.", 5) end --- Cleanup after either mission is ended or aborted function veafCasMission.cleanupAfterMission() veaf.loggers.get(veafCasMission.Id):trace("skipCasTarget START") -- destroy vehicles and infantry groups veaf.loggers.get(veafCasMission.Id):trace("destroy CAS group") local group = Group.getByName(veafCasMission.casGroupName) if group and group:isExist() == true then group:destroy() end veaf.loggers.get(veafCasMission.Id):trace("destroy AFAC group") group = Group.getByName(veafCasMission.afacName) if group and group:isExist() == true then group:destroy() end veafCasMission.afacName = nil -- remove the watchdog function veaf.loggers.get(veafCasMission.Id):trace("remove the watchdog function") if veafCasMission.groupAliveCheckTaskID ~= 'none' then mist.removeFunction(veafCasMission.groupAliveCheckTaskID) end veafCasMission.groupAliveCheckTaskID = 'none' veaf.loggers.get(veafCasMission.Id):trace("update the radio menu 1") veafRadio.delCommand(veafCasMission.rootPath, 'Target information') veaf.loggers.get(veafCasMission.Id):trace("update the radio menu 2") veafRadio.delCommand(veafCasMission.rootPath, 'Skip current objective') veaf.loggers.get(veafCasMission.Id):trace("update the radio menu 3") veafRadio.delCommand(veafCasMission.rootPath, 'Get current objective situation') veaf.loggers.get(veafCasMission.Id):trace("update the radio menu 4") veafRadio.delSubmenu(veafCasMission.targetMarkersPath, veafCasMission.rootPath) veafRadio.refreshRadioMenu() veaf.loggers.get(veafCasMission.Id):trace("skipCasTarget DONE") end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Build the initial radio menu function veafCasMission.buildRadioMenu() veafCasMission.rootPath = veafRadio.addSubMenu(veafCasMission.RadioMenuName) if not(veafRadio.skipHelpMenus) then veafRadio.addCommandToSubmenu("HELP", veafCasMission.rootPath, veafCasMission.help, nil, veafRadio.USAGE_ForGroup) end end function veafCasMission.help(unitName) local text = 'Create a marker and type "_cas" in the text\n' .. 'This will create a default CAS target group\n' .. 'You can add options (comma separated) :\n' .. ' "defense 0" completely disables air defenses\n' .. ' "defense [1-5]" specifies air defense cover (1 = light, 5 = heavy)\n' .. ' "size [1-5]" changes the group size (1 = small, 5 = huge)\n' .. ' "armor [1-5]" specifies armor presence (1 = light, 5 = heavy)\n' .. ' "spacing [1-5]" changes the groups spacing (1 = dense, 3 = default, 5 = sparse)' veaf.outTextForGroup(unitName, text, 30) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafCasMission.initialize() veafCasMission.buildRadioMenu() veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafCasMission.onEventMarkChange) end veaf.loggers.get(veafCasMission.Id):info(string.format("Loading version %s", veafCasMission.Version)) --- Enable/Disable error boxes displayed on screen. env.setErrorMessageBoxEnabled(false) ------------------ END script veafCasMission.lua ------------------ ------------------ START script veafCombatMission.lua ------------------ ------------------------------------------------------------------ -- VEAF combat mission functions for DCS World -- By zip (2020) -- -- Features: -- --------- -- * A combat mission consists in spawning enemy aircrafts -- * It also contains a mass briefing, optional objectives (timed, number of kills, ...) and can trigger the activation of one or more combat zones -- * For each mission, a specific radio sub-menu is created, allowing common actions (get mission status, weather, briefing, start and stop the mission, etc.) -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafCombatMission = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafCombatMission.Id = "COMBATMISSION" --- Version. veafCombatMission.Version = "2.2.0" -- trace level, specific to this module --veafCombatMission.LogLevel = "trace" veaf.loggers.new(veafCombatMission.Id, veafCombatMission.LogLevel) --- Number of seconds between each check of the watchdog function veafCombatMission.SecondsBetweenWatchdogChecks = 30 veafCombatMission.RadioMenuName = "MISSIONS" veafCombatMission.MinimumSpacingBetweenClones = 300 -- minimum spawn distance between clones of a group veafCombatMission.RemoteCommandParser = "([[a-zA-Z0-9]+)%s?([^%s]*)%s?(.*)" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Radio menus paths veafCombatMission.rootPath = nil -- Missions list (table of VeafCombatMission objects) veafCombatMission.missionsList = {} -- Missions dictionary (map of VeafCombatMission objects by mission name) veafCombatMission.missionsDict = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafCombatMissionObjective object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafCombatMissionObjective = {} VeafCombatMissionObjective.FAILED = -1 VeafCombatMissionObjective.SUCCESS = 1 VeafCombatMissionObjective.NOTHING = 0 function VeafCombatMissionObjective:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object -- technical name objectToCreate.name = nil -- description for the briefing objectToCreate.description = nil -- message when the objective is completed objectToCreate.message = nil -- parameters objectToCreate.parameters = {} -- function that is call when the mission starts objectToCreate.onStartupFunction = nil -- function that is called when the completion check watchdog runs (should check for objective completion and return one of the FAILED, SUCCESS or NOTHING constants) objectToCreate.onCheckFunction = nil return objectToCreate end function VeafCombatMissionObjective:copy() local copy = VeafCombatMissionObjective:new() -- copy the attributes copy.name = self.name copy.description = self.description copy.onStartupFunction = self.onStartupFunction copy.onCheckFunction = self.onCheckFunction -- deep copy the collections copy.parameters = {} for name, value in pairs(self.parameters) do veaf.loggers.get(veafCombatMission.Id):trace(string.format("copying parameter %s : ",tostring(name))) copy.parameters[name]=value end return copy end --- --- setters and getters --- function VeafCombatMissionObjective:setName(value) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective.setName([%s])",value or "")) self.name = value return self end function VeafCombatMissionObjective:getName() return self.name end function VeafCombatMissionObjective:setDescription(value) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].setDescription([%s])", self:getName() or "", value or "")) self.description = value return self end function VeafCombatMissionObjective:getDescription() return self.description end function VeafCombatMissionObjective:setMessage(value) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].setMessage([%s])", self:getName() or "", value or "")) self.message = value return self end function VeafCombatMissionObjective:getMessage() return self.message end function VeafCombatMissionObjective:setParameters(value) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].setParameters([%s])", self:getName() or "", veaf.p(value or ""))) self.parameters = value return self end function VeafCombatMissionObjective:getParameters() return self.parameters end function VeafCombatMissionObjective:setOnCheck(value) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].setOnCheck(some function)",self:getName())) self.onCheckFunction = value return self end function VeafCombatMissionObjective:getOnCheck() return self.onCheckFunction end function VeafCombatMissionObjective:setOnStartup(value) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].setOnStartup(some function)", self:getName())) self.onStartupFunction = value return self end function VeafCombatMissionObjective:getOnStartup() return self.onStartupFunction end --- --- other methods --- function VeafCombatMissionObjective:onCheck(mission) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].onCheck([%s])", self:getName() or "", mission:getName() or "")) if self.onCheckFunction then return self.onCheckFunction(mission, self.parameters) else return VeafCombatMissionObjective.NOTHING end end function VeafCombatMissionObjective:onStartup(mission) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].onStartup([%s])", self:getName() or "", mission:getName() or "")) if self.onStartupFunction then return self.onStartupFunction(self.parameters) end end function VeafCombatMissionObjective:configureAsTimedObjective(timeInSeconds) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].configureAsTimedObjective()",self:getName())) local function onCheck(mission, parameters) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective.NewTimedObjective.onCheck()")) local timeout = parameters.timeout local startTime = parameters.startTime if timer.getTime() > startTime + timeout then return VeafCombatMissionObjective.FAILED else return VeafCombatMissionObjective.NOTHING end end return self :setParameters({timeout=timeInSeconds}) :setOnStartup( function(parameters) parameters["startTime"] = timer.getTime() end ) :setOnCheck(onCheck) end function VeafCombatMissionObjective:configureAsKillEnemiesObjective(nbKillsToWin, whatsInAKill) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].configureAsKillEnemiesObjective()",self:getName())) local function onCheck(mission, parameters) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective.configureAsKillEnemiesObjective.onCheck()")) if mission:isActive() then local nbKillsToWin = parameters.nbKillsToWin local whatsInAKill = parameters.whatsInAKill veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbKillsToWin = %d",nbKillsToWin)) veaf.loggers.get(veafCombatMission.Id):trace(string.format("whatsInAKill = %d",whatsInAKill)) local nbLiveUnits, nbDamagedUnits, nbDeadUnits = mission:getRemainingEnemies(whatsInAKill) veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbLiveUnits = %d",nbLiveUnits)) veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbDamagedUnits = %d",nbDamagedUnits)) veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbDeadUnits = %d",nbDeadUnits)) if (nbKillsToWin == -1 and nbLiveUnits == 0) or (nbKillsToWin >= 0 and nbDeadUnits >= nbKillsToWin) then -- objective is achieved veaf.loggers.get(veafCombatMission.Id):trace(string.format("objective is achieved")) local msg = string.format(self:getMessage(), nbDeadUnits) if not mission:isSilent() then trigger.action.outText(msg, 15) end return VeafCombatMissionObjective.SUCCESS else veaf.loggers.get(veafCombatMission.Id):trace(string.format("objective is NOT achieved")) return VeafCombatMissionObjective.NOTHING end end return VeafCombatMissionObjective.NOTHING end return self :setParameters({nbKillsToWin=nbKillsToWin or -1, whatsInAKill=whatsInAKill or 0}) :setOnCheck(onCheck) end function VeafCombatMissionObjective:configureAsPreventDestructionOfSceneryObjectsInZone(zones, objects) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].configureAsPreventDestructionOfSceneryObjectsInZone()",self:getName())) local function onCheck(mission, parameters) veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective.configureAsPreventDestructionOfSceneryObjectsInZone.onCheck()")) if mission:isActive() then local zones = parameters.zones local objects = parameters.objects local failed = false local killedObjectsNames = nil local killedObjects = mist.getDeadMapObjsInZones(zones) ----veaf.loggers.get(veafCombatMission.Id):trace(veaf.serialize("killedObjects", killedObjects)) for _, object in pairs(killedObjects) do veaf.loggers.get(veafCombatMission.Id):trace(string.format("checking id_ = [%s]", object.object.id_)) if objects[object.object.id_] then veaf.loggers.get(veafCombatMission.Id):trace(string.format("found [%s]", objects[object.object.id_])) if killedObjectsNames then killedObjectsNames = killedObjectsNames .. ", " .. objects[object.object.id_] else killedObjectsNames = objects[object.object.id_] end failed = true end end if failed then -- objective is failed veaf.loggers.get(veafCombatMission.Id):trace(string.format("objective is failed")) local msg = string.format(self:getMessage(), killedObjectsNames) if not mission:isSilent() then trigger.action.outText(msg, 15) end return VeafCombatMissionObjective.FAILED else veaf.loggers.get(veafCombatMission.Id):trace(string.format("objective is NOT failed")) return VeafCombatMissionObjective.NOTHING end end return VeafCombatMissionObjective.NOTHING end return self :setParameters({zones=zones, objects=objects}) :setOnCheck(onCheck) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafCombatMissionElement object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafCombatMissionElement = {} function VeafCombatMissionElement:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object -- name objectToCreate.name = nil -- groups : a list of group names that compose this element objectToCreate.groups = {} -- skill ("Average", "Good", "High", "Excellent" or "Random"), defaults to "Random" objectToCreate.skill = "Random" -- SPAWN:InitSkill(Skill) -- spawn radius in meters (randomness introduced in the respawn mechanism) objectToCreate.spawnRadius = 0 -- SPAWN:InitRandomizePosition -- spawn chance in percent (xx chances in 100 that the unit is spawned - or the command run) objectToCreate.spawnChance = 100 -- the element can be multiplied to scale the mission objectToCreate.scale = 1 -- if true, the element is scalable objectToCreate.scalable = true -- init tables (no need to init simple values) return objectToCreate end function VeafCombatMissionElement:copy() local copy = VeafCombatMissionElement:new() -- copy the attributes copy.name = self.name copy.skill = self.skill copy.spawnRadius = self.spawnRadius copy.spawnChance = self.spawnChance copy.scale = self.scale copy.scalable = self.scalable -- deep copy the collections copy.groups = {} for _, group in pairs(self.groups) do table.insert(copy.groups, group) end copy.spawnPoints = {} for groupName, spawnPoint in pairs(self.spawnPoints) do copy.spawnPoints[groupName] = spawnPoint end return copy end --- --- setters and getters --- function VeafCombatMissionElement:setName(value) self.name = value return self end function VeafCombatMissionElement:getName() return self.name end function VeafCombatMissionElement:setGroups(value) veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMissionElement[%s]:setGroups(%s)",veaf.p(self.name), veaf.p(value))) self.groups = value self.spawnPoints = {} for _, groupName in pairs(self.groups) do veaf.loggers.get(veafCombatMission.Id):trace(string.format("processing groupName=%s",veaf.p(groupName))) local _group = Group.getByName(groupName) veaf.loggers.get(veafCombatMission.Id):trace(string.format("_group=%s",veaf.p(_group))) if _group then local _unit1 = _group:getUnit(1) veaf.loggers.get(veafCombatMission.Id):trace(string.format("_unit1=%s",veaf.p(_unit1))) if _unit1 then veaf.loggers.get(veafCombatMission.Id):trace(string.format("_unit1:getPoint()=%s",veaf.p(_unit1:getPoint()))) self.spawnPoints[groupName] = _unit1:getPoint() end end end return self end function VeafCombatMissionElement:getGroups() return self.groups end function VeafCombatMissionElement:setSkill(value) self.skill = value return self end function VeafCombatMissionElement:getSkill() return self.skill end function VeafCombatMissionElement:setSpawnRadius(value) self.spawnRadius = tonumber(value) return self end function VeafCombatMissionElement:getSpawnRadius() return self.spawnRadius end function VeafCombatMissionElement:setSpawnChance(value) self.spawnChance = tonumber(value) return self end function VeafCombatMissionElement:getSpawnChance() return self.spawnChance end function VeafCombatMissionElement:setScale(value) self.scale = tonumber(value) return self end function VeafCombatMissionElement:getScale() return self.scale end function VeafCombatMissionElement:setScalable(value) self.scalable = value return self end function VeafCombatMissionElement:isScalable() return self.scalable end --- --- other methods --- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafCombatMission object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafCombatMission = { -- mission name (technical) name = nil, -- mission name (human-friendly) friendlyName = nil, -- mission briefing briefing = nil, -- secured : if true, the radio menu will be secured secured = false, -- list of objectives objectives = {}, -- list of the elements defined in the mission elements = {}, -- mission is active active = false, -- mission is a training mission training = false, -- DCS groups that have been spawned (for cleaning up later) spawnedGroups = {}, --- Radio menus paths radioMarkersPath = nil, radioTargetInfoPath = nil, radioRootPath = nil, -- the watchdog function checks for mission objectives completion watchdogFunctionId = nil, -- if false, the mission will not appear in the radio menu radioMenuEnabled = false, -- if true, no message will be displayed when activating/deactivating the mission hidden = false, -- same as hidden but only valid for one activation of the mission (will be reset to *hidden* at next start) silent = false, spawnedUnitsCountByGroup = {}, spawnedNamesIndex = {} } function VeafCombatMission:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object -- mission name (technical) objectToCreate.name = nil -- mission name (human-friendly) objectToCreate.friendlyName = nil -- mission briefing objectToCreate.briefing = nil -- secured : if true, the radio menu will be secured objectToCreate.secured = false -- list of objectives objectToCreate.objectives = {} -- list of the elements defined in the mission objectToCreate.elements = {} -- mission is active objectToCreate.active = false -- mission is a training mission objectToCreate.training = false -- DCS groups that have been spawned (for cleaning up later) objectToCreate.spawnedGroups = {} --- Radio menus paths objectToCreate.radioMarkersPath = nil objectToCreate.radioTargetInfoPath = nil objectToCreate.radioRootPath = nil -- the watchdog function checks for mission objectives completion objectToCreate.watchdogFunctionId = nil -- if false, the mission will not appear in the radio menu objectToCreate.radioMenuEnabled = false -- if true, no message will be displayed when activating/deactivating the mission objectToCreate.hidden = false -- same as hidden but only valid for one activation of the mission (will be reset to *hidden* at next start) objectToCreate.silent = false objectToCreate.spawnedUnitsCountByGroup = {} objectToCreate.spawnedNamesIndex = {} return objectToCreate end function VeafCombatMission:copy(newSkill, newScale) local copy = VeafCombatMission:new() -- copy the attributes copy.name = self.name copy.friendlyName = self.friendlyName copy.secured = self.secured copy.briefing = self.briefing copy.active = self.active copy.training = self.training copy.hidden = self.hidden copy.radioMenuEnabled = self.radioMenuEnabled copy.silent = self.silent -- deep copy the collections for _, objective in pairs(self.objectives) do copy:addObjective(objective:copy()) end for _, element in pairs(self.elements) do local elementCopy = element:copy() if element:isScalable() and newScale then elementCopy:setScale(newScale) end if newSkill then elementCopy:setSkill(newSkill) end copy:addElement(elementCopy) end return copy end --- --- setters and getters --- function VeafCombatMission:setName(value) self.name = value return self end function VeafCombatMission:getName() return self.name end function VeafCombatMission:setSecured(value) self.secured = value return self end function VeafCombatMission:isSecured() return self.secured end function VeafCombatMission:getRadioMenuName() return self:getFriendlyName() end function VeafCombatMission:setFriendlyName(value) self.friendlyName = value return self end function VeafCombatMission:getFriendlyName() return self.friendlyName end function VeafCombatMission:setBriefing(value) self.briefing = value return self end function VeafCombatMission:getBriefing() return self.briefing end function VeafCombatMission:isActive() return self.active end function VeafCombatMission:setActive(value) self.active = value return self end function VeafCombatMission:isTraining() return self.training end function VeafCombatMission:setTraining(value) self.training = value return self end function VeafCombatMission:addElement(value) table.insert(self.elements, value) return self end function VeafCombatMission:addSpawnedGroup(group) veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:addSpawnedGroup(%s)",self.name or "", group:getName() or "")) if not self.spawnedGroups then self.spawnedGroups = {} end table.insert(self.spawnedGroups, group) -- count units in group self.spawnedUnitsCountByGroup[group:getName()] = #group:getUnits() veaf.loggers.get(veafCombatMission.Id):trace(string.format("%s units in group [%s]",tostring(self.spawnedUnitsCountByGroup[group:getName()]), tostring(group:getName()))) return self end function VeafCombatMission:getSpawnedGroups() veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:getSpawnedGroups()",self.name or "")) for _, group in pairs(self.spawnedGroups) do veaf.loggers.get(veafCombatMission.Id):trace(string.format("spawnedGroups[%s]",group:getName())) end return self.spawnedGroups end function VeafCombatMission:clearSpawnedGroups() self.spawnedGroups = {} return self end function VeafCombatMission:addObjective(objective) veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:addObjective(%s)",self.name or "", objective:getName() or "")) if not self.objectives then self.objectives = {} end table.insert(self.objectives, objective) return self end function VeafCombatMission:isHidden() return self.hidden end function VeafCombatMission:setHidden(value) self.hidden = value return self end function VeafCombatMission:isSilent() return self.silent end function VeafCombatMission:setSilent(value) self.silent = value return self end function VeafCombatMission:isRadioMenuEnabled() return self.radioMenuEnabled end function VeafCombatMission:setRadioMenuEnabled(value) self.radioMenuEnabled = value return self end function VeafCombatMission:setAllElementsSkill(skill) for _, element in self.elements do element:setSkill(skill) end return self end --- --- other methods --- function VeafCombatMission:scheduleWatchdogFunction() veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:scheduleWatchdogFunction()",self.name or "")) self.watchdogFunctionId = mist.scheduleFunction(veafCombatMission.CompletionCheck,{self.name},timer.getTime()+veafCombatMission.SecondsBetweenWatchdogChecks) return self end function VeafCombatMission:unscheduleWatchdogFunction() veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:unscheduleWatchdogFunction()",self.name or "")) if self.watchdogFunctionId then veaf.loggers.get(veafCombatMission.Id):debug(string.format("mist.removeFunction()")) mist.removeFunction(self.watchdogFunctionId) self.watchdogFunctionId = nil end return self end function VeafCombatMission:getObjectives() return self.objectives end function VeafCombatMission:addDefaultObjectives() -- TODO return self end function VeafCombatMission:initialize() veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:initialize()",self.name or "")) -- check parameters if not self.name then return self end if not self.friendlyName then self:setFriendlyName(self.name) end if #self.objectives == 0 then self:addDefaultObjectives() end -- refresh the radio menu self:updateRadioMenu() return self end function VeafCombatMission:getRemainingEnemiesString() local nbLiveUnits, nbDamagedUnits, nbDeadUnits = self:getRemainingEnemies() return string.format("%d alive (%d damaged), %d dead", nbLiveUnits, nbDamagedUnits, nbDeadUnits) end function VeafCombatMission:getRemainingEnemies(whatsInAKill) local whatsInAKill = whatsInAKill or 0.01 local nbLiveUnits = 0 local nbDamagedUnits = 0 local nbDeadUnits = 0 for _, group in pairs(self:getSpawnedGroups()) do veaf.loggers.get(veafCombatMission.Id):trace(string.format("processing group [%s]",group:getName())) local groupLiveUnits = 0 local groupDamagedUnits = 0 if group and group:getUnits() then for _, unit in pairs(group:getUnits()) do veaf.loggers.get(veafCombatMission.Id):trace(string.format("processing unit [%s]",unit:getName())) veaf.loggers.get(veafCombatMission.Id):trace(string.format("veaf.getUnitLifeRelative(unit) = %f",veaf.getUnitLifeRelative(unit))) if veaf.getUnitLifeRelative(unit) == 1.0 then veaf.loggers.get(veafCombatMission.Id):trace(string.format("unit[%s] is alive",unit:getName())) groupLiveUnits = groupLiveUnits + 1 elseif veaf.getUnitLifeRelative(unit) > whatsInAKill then veaf.loggers.get(veafCombatMission.Id):trace(string.format("unit[%s] is damaged (%d %%)",unit:getName(), veaf.getUnitLifeRelative(unit)*100 )) groupDamagedUnits = groupDamagedUnits + 1 groupLiveUnits = groupLiveUnits + 1 else veaf.loggers.get(veafCombatMission.Id):trace(string.format("unit[%s] is dead",unit:getName())) -- should never come to that, Moose do not return dead units in getUnits() end end else groupLiveUnits = 0 end local groupDeadUnits = (self.spawnedUnitsCountByGroup[group:getName()] or 0) - groupLiveUnits if groupDeadUnits < 0 then -- should never happen but who knows ? This is DCS ! groupDeadUnits = 0 end veaf.loggers.get(veafCombatMission.Id):trace(string.format("groupLiveUnits = %d",groupLiveUnits)) veaf.loggers.get(veafCombatMission.Id):trace(string.format("groupDamagedUnits = %d",groupDamagedUnits)) veaf.loggers.get(veafCombatMission.Id):trace(string.format("groupDeadUnits = %d",groupDeadUnits)) nbLiveUnits = nbLiveUnits + groupLiveUnits nbDamagedUnits = nbDamagedUnits + groupDamagedUnits nbDeadUnits = nbDeadUnits + groupDeadUnits end veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbLiveUnits = %d",nbLiveUnits)) veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbDamagedUnits = %d",nbDamagedUnits)) veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbDeadUnits = %d",nbDeadUnits)) return nbLiveUnits, nbDamagedUnits, nbDeadUnits end function VeafCombatMission:getInformation() veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:getInformation()",self.name or "")) local message = "COMBAT MISSION "..self:getFriendlyName().." \n\n" if (self:getBriefing()) then message = message .. "BRIEFING: \n" message = message .. self:getBriefing() message = message .. "\n\n" end if (self:getObjectives() and #self:getObjectives() > 0) then message = message .. "OBJECTIVES: \n" for _, objective in pairs(self:getObjectives()) do message = message .. " - " .. objective:getDescription() .. "\n" end message = message .. "\n\n" end if self:isActive() then -- generate information dispatch message = message .. "ENEMIES : " ..self:getRemainingEnemiesString() .."\n" if self:isTraining() then -- TODO find the position of the enemies end else message = message .. "mission is not yet active." end return message end -- activate the mission function VeafCombatMission:activate(silent) veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:activate(%s)",self:getName(), tostring(silent))) -- don't start twice if self:isActive() then return nil end self:setActive(true) self:setSilent(self:isHidden() or silent) for _, missionElement in pairs(self.elements) do veaf.loggers.get(veafCombatMission.Id):debug(string.format("processing element [%s]",missionElement:getName())) local chance = math.random(0, 100) if chance <= missionElement:getSpawnChance() then -- spawn the element veaf.loggers.get(veafCombatMission.Id):debug(string.format("chance hit (%d <= %d)",chance, missionElement:getSpawnChance())) for _, groupName in pairs(missionElement:getGroups()) do local _spawnPoint = missionElement.spawnPoints[groupName] veaf.loggers.get(veafCombatMission.Id):trace(string.format("_spawnPoint=%s",veaf.p(_spawnPoint))) local _spawnRadius = missionElement:getSpawnRadius() if (missionElement:getScale() > 1 and _spawnRadius < veafCombatMission.MinimumSpacingBetweenClones) then _spawnRadius = veafCombatMission.MinimumSpacingBetweenClones end veaf.loggers.get(veafCombatMission.Id):trace(string.format("_spawnRadius=%s",veaf.p(_spawnRadius))) local vars = {} vars.gpName = groupName vars.action = 'clone' vars.point = _spawnPoint vars.radius = _spawnRadius vars.disperse = false vars.route = mist.getGroupRoute(groupName, 'task') --veaf.loggers.get(veafCombatMission.Id):trace(string.format("vars=%s",veaf.p(vars))) for i=1,missionElement:getScale() do if not self.spawnedNamesIndex[groupName] then self.spawnedNamesIndex[groupName] = 1 else self.spawnedNamesIndex[groupName] = self.spawnedNamesIndex[groupName] + 1 end local spawnedGroupName = string.format("%s #%04d", groupName, self.spawnedNamesIndex[groupName]) veaf.loggers.get(veafCombatMission.Id):trace(string.format("spawnedGroupName=%s",veaf.p(spawnedGroupName))) local _group = mist.teleportToPoint(vars, true) if _group then for _, unit in pairs(_group.units) do unit.skill = missionElement:getSkill() end end _group.groupName = spawnedGroupName if _group then for _, unit in pairs(_group.units) do local unitName = unit.unitName veaf.loggers.get(veafCombatMission.Id):trace(string.format("unitName=%s",veaf.p(unitName))) if not self.spawnedNamesIndex[unitName] then self.spawnedNamesIndex[unitName] = 1 else self.spawnedNamesIndex[unitName] = self.spawnedNamesIndex[unitName] + 1 end local spawnedUnitName = string.format("%s #%04d", unitName, self.spawnedNamesIndex[unitName]) unit.groupName = spawnedUnitName veaf.loggers.get(veafCombatMission.Id):trace(string.format("spawnedUnitName=%s",veaf.p(spawnedUnitName))) end end veaf.loggers.get(veafCombatMission.Id):trace(string.format("_group=%s",veaf.p(_group))) local _spawnedGroup = mist.dynAdd(_group) if _spawnedGroup then veaf.loggers.get(veafCombatMission.Id):trace(string.format("_spawnedGroup.name=%s",veaf.p(_spawnedGroup.name))) local _dcsSpawnedGroup = Group.getByName(_spawnedGroup.name) veaf.loggers.get(veafCombatMission.Id):trace(string.format("_spawnedGroup.name=%s",veaf.p(_dcsSpawnedGroup:getName()))) for _, unit in pairs(_dcsSpawnedGroup:getUnits()) do veaf.loggers.get(veafCombatMission.Id):trace(string.format("_spawnedGroup.unit.name=%s",veaf.p(unit:getName()))) end self:addSpawnedGroup(_dcsSpawnedGroup) -- add the group to the Hound Elint, if there is one if veafHoundElint then veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafHoundElint.addPlatformToSystem(%s)",veaf.p(_dcsSpawnedGroup:getName()))) veafHoundElint.addPlatformToSystem(_dcsSpawnedGroup) end end end end else veaf.loggers.get(veafCombatMission.Id):debug(string.format("chance missed (%d > %d)",chance, missionElement:getSpawnChance())) end end -- start all the objectives for _, objective in pairs(self.objectives) do objective:onStartup(self) end -- start the completion watchdog self:scheduleWatchdogFunction() -- refresh the radio menu self:updateRadioMenu() return self end -- desactivate the mission function VeafCombatMission:desactivate() veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:desactivate()",self.name or "")) self:setActive(false) self:unscheduleWatchdogFunction() for _, group in pairs(self:getSpawnedGroups()) do veaf.loggers.get(veafCombatMission.Id):trace(string.format("trying to destroy group [%s]",group:getName())) group:destroy() end self:clearSpawnedGroups() -- refresh the radio menu self:updateRadioMenu() return self end -- check if there are still units in mission function VeafCombatMission:completionCheck() veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:completionCheck()",self.name or "")) VeafCombatMissionObjective.FAILED = -1 VeafCombatMissionObjective.SUCCESS = 1 VeafCombatMissionObjective.NOTHING = 0 local reschedule = true -- check all the objectives for _, objective in pairs(self.objectives) do local result = objective:onCheck(self) if result == VeafCombatMissionObjective.FAILED then -- mission is failed local message = string.format([[ Objective not met : %s The mission %s will now end. You can replay by starting it again, in the radio menu.]], objective:getDescription(), self:getFriendlyName()) if not self:isSilent() then trigger.action.outText(message, 15) end self:desactivate() reschedule = false elseif result == VeafCombatMissionObjective.SUCCESS then -- mission is won local message = string.format([[ All objectives were met ! The mission %s is a success ! It will now end. You can replay by starting it again, in the radio menu.]], self:getFriendlyName()) if not self:isSilent() then trigger.action.outText(message, 15) end self:desactivate() reschedule = false end end if reschedule then -- reschedule self:scheduleWatchdogFunction() end end -- updates the radio menu according to the mission state function VeafCombatMission:updateRadioMenu(inBatch) veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:updateRadioMenu(%s)",self.name or "", tostring(inBatch))) -- do not update the radio menu for a mission that has no menu if not self:isRadioMenuEnabled() then return self end -- do not update the radio menu if not yet initialized if not veafCombatMission.rootPath then return self end -- reset the radio menu if self.radioRootPath then veaf.loggers.get(veafCombatMission.Id):trace("reset the radio submenu") veafRadio.clearSubmenu(self.radioRootPath) end -- populate the radio menu veaf.loggers.get(veafCombatMission.Id):trace("populate the radio menu") -- global commands veafRadio.addCommandToSubmenu("Get info", self.radioRootPath, veafCombatMission.GetInformationOnMission, self.name, veafRadio.USAGE_ForAll) if self:isActive() then -- mission is active, set up accordingly (desactivate mission, get information, pop smoke, etc.) veaf.loggers.get(veafCombatMission.Id):trace("mission is active") if self:isSecured() then veafRadio.addSecuredCommandToSubmenu('Desactivate mission', self.radioRootPath, veafCombatMission.DesactivateMission, self.name, veafRadio.USAGE_ForAll) else veafRadio.addCommandToSubmenu('Desactivate mission', self.radioRootPath, veafCombatMission.DesactivateMission, self.name, veafRadio.USAGE_ForAll) end else -- mission is not active, set up accordingly (activate mission) veaf.loggers.get(veafCombatMission.Id):trace("mission is not active") if self:isSecured() then veafRadio.addSecuredCommandToSubmenu('Activate mission', self.radioRootPath, veafCombatMission.ActivateMission, self.name, veafRadio.USAGE_ForAll) else veafRadio.addCommandToSubmenu('Activate mission', self.radioRootPath, veafCombatMission.ActivateMission, self.name, veafRadio.USAGE_ForAll) end end if not inBatch then veafRadio.refreshRadioMenu() end return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- global functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafCombatMission.GetMissionNumber(number) veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.GetMissionNumber([%s])",tostring(number))) local mission = veafCombatMission.missionsList[number] return mission end function veafCombatMission.GetMission(name) veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.GetMission([%s])",name or "")) veaf.loggers.get(veafCombatMission.Id):debug(string.format("Searching for mission with name [%s]", name)) local mission = veafCombatMission.missionsDict[name:lower()] if not mission then local message = string.format("VeafCombatMission [%s] was not found !",name) veaf.loggers.get(veafCombatMission.Id):error(message) trigger.action.outText(message,5) end return mission end -- add a mission function veafCombatMission.AddMission(mission) veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.AddMission([%s])",mission:getName() or "")) veaf.loggers.get(veafCombatMission.Id):debug(string.format("Adding mission [%s]", mission:getName())) mission:initialize() table.insert(veafCombatMission.missionsList, mission) veafCombatMission.missionsDict[mission:getName():lower()] = mission return mission end -- add a mission and create copies with different skills function veafCombatMission.AddMissionsWithSkillAndScale(mission, includeOriginal, skills, scales) veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.AddMissionsWithSkill([%s])",mission:getName() or "")) veaf.loggers.get(veafCombatMission.Id):trace(string.format("skills=%s",veaf.p(skills))) veaf.loggers.get(veafCombatMission.Id):trace(string.format("scales=%s",veaf.p(scales))) if (mission:isRadioMenuEnabled() and includeOriginal) then veafCombatMission.AddMission(mission) end local skills = skills or {"Average", "Good", "High", "Excellent", "Random"} local scales = scales or {1, 2, 3, 4} for _, scale in pairs(scales) do for _, skill in pairs(skills) do local copy = mission:copy(skill, scale)--:setRadioMenuEnabled(false) copy:setName(mission:getName().."/"..skill.."/"..scale) copy:setFriendlyName(mission:getFriendlyName()) veafCombatMission.AddMission(copy) end end return mission end -- activate a mission by number function veafCombatMission.ActivateMissionNumber(number, silent) local mission = veafCombatMission.GetMissionNumber(number) if mission then veafCombatMission.ActivateMission(mission:getName(), silent) end end -- activate a mission function veafCombatMission.ActivateMission(name, silent, unitName) veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.ActivateMission([%s])",name or "")) local mission = veafCombatMission.GetMission(name) local result = mission:activate(silent) if not silent and not mission:isSilent() then if result then veaf.outTextForUnit(unitName, "VeafCombatMission "..mission:getFriendlyName().." has been activated.", 10) mist.scheduleFunction(veafCombatMission.GetInformationOnMission,{{name}},timer.getTime()+1) else veaf.outTextForUnit(unitName, "VeafCombatMission "..mission:getFriendlyName().." was already active.", 10) end end veafCombatMission.buildRadioMenu() end -- desactivate a mission by number function veafCombatMission.DesactivateMissionNumber(number, silent) local mission = veafCombatMission.GetMission(number) if mission then veafCombatMission.DesactivateMission(mission:getName(), silent) end end -- desactivate a mission function veafCombatMission.DesactivateMission(name, silent, unitName) veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.DesactivateMission([%s])",name or "")) local mission = veafCombatMission.GetMission(name) mission:desactivate() if not silent and not mission:isSilent() then veaf.outTextForUnit(unitName, "VeafCombatMission "..mission:getFriendlyName().." has been desactivated.", 10) end veafCombatMission.buildRadioMenu() end -- print information about a mission function veafCombatMission.GetInformationOnMission(parameters) local name, unitName = veaf.safeUnpack(parameters) veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.GetInformationOnMission([%s])",name or "")) local mission = veafCombatMission.GetMission(name) local text = mission:getInformation() if unitName then veaf.outTextForGroup(unitName, text, 30) else trigger.action.outText(text, 30) end end -- call the completion watchdog methods function veafCombatMission.CompletionCheck(name) veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.CompletionCheck([%s])",name or "")) local mission = veafCombatMission.GetMission(name) mission:completionCheck() end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- local function _groupMissions() local missionGroups = {} local activeGroups = {} for _, mission in pairs(veafCombatMission.missionsDict) do veaf.loggers.get(veafCombatMission.Id):trace(string.format("grouping missionName=%s", mission:getName())) if mission:isRadioMenuEnabled() then local regex = ("^([^/]+)/([^/]+)/(.+)$") local name, skill, scale = mission:getName():match(regex) veaf.loggers.get(veafCombatMission.Id):trace(string.format("name=%s, skill=%s, scale=%s", tostring(name), tostring(skill), tostring(scale))) local groupName = name if not groupName then groupName = mission:getName() end if not(missionGroups[groupName]) then missionGroups[groupName] = {} --veaf.loggers.get(veafCombatMission.Id):trace(string.format("creating group %s", groupName)) end table.insert(missionGroups[groupName], mission) if mission:isActive() then veaf.loggers.get(veafCombatMission.Id):trace(string.format("mission %s is active", mission:getName())) veaf.loggers.get(veafCombatMission.Id):trace(string.format("activating group %s", groupName)) if not(activeGroups[groupName]) then activeGroups[groupName] = {} end if skill then veaf.loggers.get(veafCombatMission.Id):trace(string.format("activating skill %s", skill)) if not(activeGroups[groupName][skill]) then activeGroups[groupName][skill] = {} end if scale then veaf.loggers.get(veafCombatMission.Id):trace(string.format("activating scale %s", scale)) activeGroups[groupName][skill][scale] = true end end end end end --veaf.loggers.get(veafCombatMission.Id):trace(string.format("missionGroups=%s",veaf.p(missionGroups))) veaf.loggers.get(veafCombatMission.Id):trace(string.format("activeGroups=%s",veaf.p(activeGroups))) return missionGroups, activeGroups end function veafCombatMission._buildMissionRadioMenu(menu, title, element) local missions = element.missions if #missions == 1 then -- one simple mission local mission = missions[1] if mission:isActive() then title = "* "..title end mission.radioRootPath = veafRadio.addSubMenu(title, menu) mission:updateRadioMenu(true) else -- group by skill and scale veaf.loggers.get(veafCombatMission.Id):trace("group by skill and scale") local skills = {} for _, mission in pairs(missions) do local regex = ("^([^/]+)/([^/]+)/(%d+)$") local name, skill, scale = mission:getName():match(regex) veaf.loggers.get(veafCombatMission.Id):trace(string.format("missionName=[%s], name=%s, skill=%s, scale=%s", tostring(mission:getName()), tostring(name), tostring(skill), tostring(scale))) if not skills[skill] then skills[skill] = {} end skills[skill][scale] = mission end veaf.loggers.get(veafCombatMission.Id):trace(string.format("skills=%s", veaf.p(skills))) -- create the radio menus local title = title if element.activeGroups then title = "* "..title end local missionPath = veafRadio.addSubMenu(title, menu) veaf.loggers.get(veafCombatMission.Id):trace(string.format(" %s", title)) local skillsNames = {} for skill, _ in pairs(skills) do table.insert(skillsNames, skill) end table.sort(skillsNames) for _, skill in pairs(skillsNames) do local scales = skills[skill] local skillTitle = skill if element.activeGroups and element.activeGroups[skill] then skillTitle = "* "..skillTitle end local skillPath = veafRadio.addSubMenu(skillTitle, missionPath) veaf.loggers.get(veafCombatMission.Id):trace(string.format(" %s", skill)) local scalesNames = {} for scale, _ in pairs(scales) do table.insert(scalesNames, scale) end table.sort(scalesNames) for _, scale in pairs(scalesNames) do local mission = scales[scale] local scaleTitle = "scale "..scale if element.activeGroups and element.activeGroups[skill] and element.activeGroups[skill][scale] then scaleTitle = "* "..scaleTitle end local scalePath = veafRadio.addSubMenu(scaleTitle, skillPath) veaf.loggers.get(veafCombatMission.Id):trace(string.format(" %s", scale)) mission.radioRootPath = scalePath mission:updateRadioMenu(true) end end end end --- Build the initial radio menu function veafCombatMission.buildRadioMenu() veaf.loggers.get(veafCombatMission.Id):debug("buildRadioMenu()") -- don't create an empty menu if veaf.length(veafCombatMission.missionsDict) == 0 then return end if veafCombatMission.rootPath then veafRadio.clearSubmenu(veafCombatMission.rootPath) else veafCombatMission.rootPath = veafRadio.addMenu(veafCombatMission.RadioMenuName) end if not(veafRadio.skipHelpMenus) then veafRadio.addCommandToSubmenu("HELP", veafCombatMission.rootPath, veafCombatMission.help, nil, veafRadio.USAGE_ForGroup) end veafRadio.addCommandToSubmenu("List available", veafCombatMission.rootPath, veafCombatMission.listAvailableMissions, nil, veafRadio.USAGE_ForAll) veafRadio.addCommandToSubmenu("List active", veafCombatMission.rootPath, veafCombatMission.listActiveMissions, nil, veafRadio.USAGE_ForAll) local missions = {} local missionGroups, activeGroups = _groupMissions() for groupName, missionsInGroup in pairs(missionGroups) do veaf.loggers.get(veafCombatMission.Id):trace(string.format("processing groupName=%s",groupName)) missions[groupName] = {title=missionsInGroup[1]:getRadioMenuName(), sort=missionsInGroup[1]:getFriendlyName(), missions=missionsInGroup, activeGroups=activeGroups[groupName]} end veaf.loggers.get(veafCombatMission.Id):trace(string.format("missions=%s",veaf.p(missions))) --veaf.loggers.get(veafCombatMission.Id):trace(string.format("#missions=%d",#missions)) veafRadio.addPaginatedRadioElements(veafCombatMission.rootPath, veafCombatMission._buildMissionRadioMenu, missions) veafRadio.refreshRadioMenu() end function veafCombatMission.help(unitName) local text = 'Combat missions are defined by the mission maker, and listed here\n' .. 'You can start and stop them at will,\n' .. 'as well as ask for information about their status.' veaf.outTextForGroup(unitName, text, 30) end function veafCombatMission.listAvailableMissions(unitName) -- sort the missions alphabetically local sortedMissions = {} local groupedMissions = _groupMissions() for groupName, missionsInGroup in pairs(groupedMissions) do table.insert(sortedMissions, groupName) end table.sort(sortedMissions) local text = 'List of all available combat missions:\n' for _, missionName in pairs(sortedMissions) do text = text .. " - " .. missionName .. "\n" end veaf.outTextForUnit(unitName, text, 20) end function veafCombatMission.listActiveMissions() -- sort the missions alphabetically local sortedMissions = {} for _, mission in pairs(veafCombatMission.missionsDict) do if mission:isActive() then table.insert(sortedMissions, mission:getName() .. ' : ' .. mission:getRemainingEnemiesString()) end end table.sort(sortedMissions) local text = 'No active combat mission !' if #sortedMissions > 0 then text = 'List of active combat missions:\n' for _, missionName in pairs(sortedMissions) do text = text .. " - " .. missionName .. "\n" end end trigger.action.outText(text, 20) end -- add a standard CAP mission with a single group function veafCombatMission.addCapMission(missionName, missionDescription, missionBriefing, secured, radioMenuEnabled, skills, scales, spawnRadius) veaf.loggers.get(veafCombatMission.Id):trace(string.format("veafCombatMission.addCapMission(%s)",tostring(missionName))) local groupName = "OnDemand-"..missionName local spawnRadius = spawnRadius if spawnRadius == nil then spawnRadius = 20000 end local secured = secured if secured == nil then secured = true end local radioMenuEnabled = radioMenuEnabled if radioMenuEnabled == nil then radioMenuEnabled = false end local skills = skills veaf.loggers.get(veafCombatMission.Id):trace(string.format("checking skills")) if not skills then veaf.loggers.get(veafCombatMission.Id):trace(string.format("skills is nil")) if radioMenuEnabled then skills = {"Good", "Excellent"} else skills = nil end end local scales = scales veaf.loggers.get(veafCombatMission.Id):trace(string.format("checking scales")) if not scales then veaf.loggers.get(veafCombatMission.Id):trace(string.format("scales is nil")) if radioMenuEnabled then scales = {1, 2} else scales = nil end end veaf.loggers.get(veafCombatMission.Id):trace(string.format("skills=(%s)", veaf.p(skills))) veaf.loggers.get(veafCombatMission.Id):trace(string.format("scales=(%s)", veaf.p(scales))) veafCombatMission.AddMissionsWithSkillAndScale( VeafCombatMission:new() :setSecured(secured) :setRadioMenuEnabled(radioMenuEnabled) :setName(missionName) :setFriendlyName(missionDescription) :setBriefing(missionBriefing) :addElement( VeafCombatMissionElement:new() :setName(groupName) :setGroups({groupName}) :setSkill("Random") :setScalable(true) :setSpawnRadius(spawnRadius) ) :addObjective( VeafCombatMissionObjective:new() :setName("Kill all the ennemies") :setDescription("you must kill all of the ennemies") :setMessage("%d ennemies destroyed !") :configureAsKillEnemiesObjective() ) :initialize() ,false, skills, scales) end function veafCombatMission.dumpMissionsList(export_path) local jsonify = function(key, value) veaf.loggers.get(veafCombatMission.Id):trace("jsonify()") veaf.loggers.get(veafCombatMission.Id):trace("key=%s", veaf.p(key)) veaf.loggers.get(veafCombatMission.Id):trace("value=%s", veaf.p(value)) if veaf.json then return veaf.json.stringify(veafCombatMission.missionsDict[value]) else return "" end end -- sort the missions alphabetically local sortedMissions = {} for _, mission in pairs(veafCombatMission.missionsDict) do veaf.loggers.get(veafCombatMission.Id):trace("mission=%s", veaf.p(mission)) table.insert(sortedMissions, mission:getName():lower()) end table.sort(sortedMissions) local _filename = "CombatMissionsList.json" if veaf.config.MISSION_NAME then _filename = "CombatMissionsList_" .. veaf.config.MISSION_NAME .. ".json" end if not(veaf.DO_NOT_EXPORT_JSON_FILES) then veaf.exportAsJson(sortedMissions, "combatMissions", jsonify, _filename, export_path) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- execute command from the remote interface function veafCombatMission.executeCommandFromRemote(parameters) veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.executeCommandFromRemote()")) veaf.loggers.get(veafCombatMission.Id):trace(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) veaf.loggers.get(veafCombatMission.Id):trace(string.format("_pilot= %s", veaf.p(_pilot))) veaf.loggers.get(veafCombatMission.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName))) veaf.loggers.get(veafCombatMission.Id):trace(string.format("_unitName= %s", veaf.p(_unitName))) veaf.loggers.get(veafCombatMission.Id):trace(string.format("_command= %s", veaf.p(_command))) if not _pilot or not _command then return false end if _command then -- parse the command local _action, _missionName, _parameters = _command:match(veafCombatMission.RemoteCommandParser) veaf.loggers.get(veafCombatMission.Id):trace(string.format("_action=%s",veaf.p(_action))) veaf.loggers.get(veafCombatMission.Id):trace(string.format("_missionName=%s",veaf.p(_missionName))) veaf.loggers.get(veafCombatMission.Id):trace(string.format("_parameters=%s",veaf.p(_parameters))) if _action and _action:lower() == "list" then veaf.loggers.get(veafCombatMission.Id):info(string.format("[%s] is listing air missions)",veaf.p(_pilot.name))) veafCombatMission.listAvailableMissions(_unitName) return true elseif _action and _action:lower() == "start" and _missionName then local _silent = _parameters and _parameters:lower() == "silent" veaf.loggers.get(veafCombatMission.Id):info(string.format("[%s] is starting air mission [%s] %s)",veaf.p(_pilot.name), veaf.p(_missionName), veaf.p(_parameters))) veafCombatMission.ActivateMission(_missionName, _silent, _unitName) return true elseif _action and _action:lower() == "stop" then local _silent = _parameters and _parameters:lower() == "silent" veaf.loggers.get(veafCombatMission.Id):info(string.format("[%s] is stopping air mission [%s] %s)",veaf.p(_pilot.name), veaf.p(_missionName), veaf.p(_parameters))) veafCombatMission.DesactivateMission(_missionName, _silent, _unitName) return true end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafCombatMission.initialize() veaf.loggers.get(veafCombatMission.Id):info("Initializing module") veafCombatMission.buildRadioMenu() veafCombatMission.dumpMissionsList(veaf.config.MISSION_EXPORT_PATH) end veaf.loggers.get(veafCombatMission.Id):info(string.format("Loading version %s", veafCombatMission.Version)) ------------------ END script veafCombatMission.lua ------------------ ------------------ START script veafCombatZone.lua ------------------ ------------------------------------------------------------------ -- VEAF combat zone functions for DCS World -- By zip (2019-20) -- -- Features: -- --------- -- * Zones can be defined in the mission editor that are then managed by this script. -- * For each zone, a specific radio sub-menu is created, allowing common actions on all specific zone (get coordinates, enemy presence, weather, pop smoke and flares, read a briefing, stop and start dynamic activity on the zone, etc.) -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafCombatZone = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafCombatZone.Id = "COMBATZONE" --- Version. veafCombatZone.Version = "1.22.0" -- trace level, specific to this module --veafCombatZone.LogLevel = "trace" veaf.loggers.new(veafCombatZone.Id, veafCombatZone.LogLevel) --- Number of seconds between each check of the zone watchdog function veafCombatZone.SecondsBetweenWatchdogChecks = 60 --- Number of seconds between each smoke request on the zones veafCombatZone.SecondsBetweenSmokeRequests = 180 --- Number of seconds between each flare request on the zones veafCombatZone.SecondsBetweenFlareRequests = 120 veafCombatZone.DefaultSpawnRadiusForUnits = 50 veafCombatZone.DefaultSpawnRadiusForStatics = 0 veafCombatZone.RadioMenuName = "COMBAT ZONES" -- Combat zones specific radio menu name veafCombatZone.CombatZoneRadioMenuName = nil -- Combat operations specific radio menu name veafCombatZone.OperationRadioMenuName = nil veafCombatZone.EventMessages = { CombatZoneComplete = [[ Well done ! All enemies in zone %s have been destroyed or routed. The zone will now be desactivated. You can replay by activating it again, in the radio menu.]], PopSmokeRequest = "Copy RED smoke requested on %s !", UseFlareRequest = "Copy illumination flare requested on %s !", CombatOperationComplete = "Operation %s is over. Congratulations !" } ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Radio menus paths veafCombatZone.rootPath = nil --- Combat Zones radio menus paths veafCombatZone.combatZoneRootPath = nil --- Operation radio menus paths veafCombatZone.operationRootPath = nil -- Zones list (table of VeafCombatZone objects) veafCombatZone.zonesList = {} -- Zones dictionary (map of VeafCombatZone objects by zone name) veafCombatZone.zonesDict = {} -- Radio groups dictionary (map of radio menu paths by radio group name) veafCombatZone.radioGroupsDict = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utils ------------------------------------------------------------------------------------------------------------------------------------------------------------- local messageSeparator = "\n=====================================================\n" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafCombatZoneElement object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafCombatZoneElement = {} function VeafCombatZoneElement:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object -- name objectToCreate.name = nil -- position on the map objectToCreate.position = nil -- if true, this is a simple dcs static objectToCreate.dcsStatic = false -- if true, this is a simple dcs group objectToCreate.dcsGroup = false -- if true, this is a VEAF command objectToCreate.veafCommand = nil -- coalition (0 = neutral, 1 = red, 2 = blue) objectToCreate.coalition = nil -- route, only for veaf commands (groups already have theirs) objectToCreate.route = nil -- spawn radius in meters (randomness introduced in the respawn mechanism) objectToCreate.spawnRadius = 0 -- spawn chance in percent (xx chances in 100 that the unit is spawned - or the command run) objectToCreate.spawnChance = 100 -- grouping elements (spawnGroup) so that a certain number (spawnCount) is guaranteed to spawn, by running the spawn random chance computation as often as necessary objectToCreate.spawnGroup = nil -- grouping elements (spawnGroup) so that a certain number (spawnCount) is guaranteed to spawn, by running the spawn random chance computation as often as necessary objectToCreate.spawnCount = 1 return objectToCreate end --- --- setters and getters --- function VeafCombatZoneElement:setName(value) self.name = value return self end function VeafCombatZoneElement:getName() return self.name end function VeafCombatZoneElement:setPosition(value) self.position = value return self end function VeafCombatZoneElement:getPosition() return self.position end function VeafCombatZoneElement:setDcsStatic(value) self.dcsStatic = value return self end function VeafCombatZoneElement:isDcsStatic() return self.dcsStatic end function VeafCombatZoneElement:setDcsGroup(value) self.dcsGroup = value return self end function VeafCombatZoneElement:isDcsGroup() return self.dcsGroup end function VeafCombatZoneElement:setVeafCommand(value) self.veafCommand = value return self end function VeafCombatZoneElement:getVeafCommand() return self.veafCommand end function VeafCombatZoneElement:setRoute(value) self.route = value return self end function VeafCombatZoneElement:getRoute() return self.route end function VeafCombatZoneElement:setCoalition(value) self.coalition = value return self end function VeafCombatZoneElement:getCoalition() return self.coalition end function VeafCombatZoneElement:setSpawnRadius(value) self.spawnRadius = tonumber(value) return self end function VeafCombatZoneElement:getSpawnRadius() return self.spawnRadius end function VeafCombatZoneElement:setSpawnChance(value) self.spawnChance = tonumber(value) return self end function VeafCombatZoneElement:getSpawnChance() return self.spawnChance end function VeafCombatZoneElement:setSpawnGroup(value) self.spawnGroup = value return self end function VeafCombatZoneElement:getSpawnGroup() return self.spawnGroup end function VeafCombatZoneElement:setSpawnDelay(value) if type(value) ~= "number" then value = tonumber(value) end self.spawnDelay = value return self end function VeafCombatZoneElement:getSpawnDelay() return self.spawnDelay end function VeafCombatZoneElement:setSpawnCount(value) self.spawnCount = tonumber(value) return self end function VeafCombatZoneElement:getSpawnCount() return self.spawnCount end --- --- other methods --- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafCombatZone object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafCombatZone = {} function VeafCombatZone:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object -- zone name (human-friendly) objectToCreate.friendlyName = nil -- technical zone name (in the mission editor) objectToCreate.missionEditorZoneName = nil -- mission briefing objectToCreate.briefing = nil -- list of defined objectives objectToCreate.objectives = {} -- list of the elements defined in the zone objectToCreate.elements = {} objectToCreate.elementGroups = {} -- the trigger zone object objectToCreate.triggerZone = nil -- the zone center objectToCreate.zoneCenter = nil -- zone is active objectToCreate.active = false -- zone is a training zone objectToCreate.training = false -- display the remaining units objectToCreate.showUnitsList = true -- display the zone coordinates and weather objectToCreate.showZonePositionInfo = true -- zone is completable (i.e. disable it when all ennemies are dead) objectToCreate.completable = true -- DCS groups that have been spawned (for cleaning up later) objectToCreate.spawnedGroups = {} objectToCreate.delayedSpawners = {} -- Whether we want the combat zone to be added to populate the radio menu objectToCreate.enableRadioMenu = true -- Whether we want the combat zone to be cleaned when it is over objectToCreate.enableJunkCleanup = true -- whether the zone can be activated/deactivated by user via radio menu. If false, the zone won't be added to radio menu until activated objectToCreate.enableUserActivation = true -- whether we want to allow ground marking of the zone objectToCreate.enableSmokeAndFlare = true -- list of chained combat zones; this is are list of combat zones, that are activated randomly objectToCreate.chainedCombatZones = nil -- delay (in seconds) between the end of this combat zone and the start of the other; can be a randomizable numeric (e.g. "[1-5]") objectToCreate.chainedCombatZonesDelay = nil --- Radio menus objectToCreate.radioGroupName = nil objectToCreate.radioMenuPrefix = nil objectToCreate.radioParentPath = nil objectToCreate.radioMarkersPath = nil objectToCreate.radioTargetInfoPath = nil objectToCreate.radioRootPath = nil -- the watchdog function checks for zone objectives completion objectToCreate.watchdogFunctionId = nil -- "pop smoke" command reset function id objectToCreate.smokeResetFunctionId = nil -- "pop flare" command reset function id objectToCreate.flareResetFunctionId = nil -- function to call when combat zone is over. The function is passed self combat zone objectToCreate.onCompletedHook = nil return objectToCreate end --- --- setters and getters --- function VeafCombatZone:setOnCompletedHook(onCompletedFunction) self.onCompletedHook = onCompletedFunction return self end function VeafCombatZone:disableRadioMenu() self.enableRadioMenu = false return self end function VeafCombatZone:disableJunkCleanup() self.enableJunkCleanup = false return self end -- make sure users cannot activate the zone (it won't be in the radio menu unless it's already active) function VeafCombatZone:disableUserActivation() self.enableUserActivation = false return self end -- make sure users can activate the zone (it will be in the radio menu even if inactive - that's the default) function VeafCombatZone:enableUserActivation() self.enableUserActivation = true return self end function VeafCombatZone:setEnableUserActivation(value) self.enableUserActivation = value return self end function VeafCombatZone:setEnableSmokeAndFlare(value) self.enableSmokeAndFlare = value return self end function VeafCombatZone:getRadioMenuName(asActive) local active = "" if asActive then active = "* " end local prefix = "" if self:getRadioMenuPrefix() then prefix = self:getRadioMenuPrefix() .. " " end return prefix .. active .. self:getFriendlyName() end function VeafCombatZone:setFriendlyName(value) self.friendlyName = value return self end function VeafCombatZone:getFriendlyName() return self.friendlyName end function VeafCombatZone:getRadioMenuPrefix() return self.radioMenuPrefix end function VeafCombatZone:setRadioMenuPrefix(value) self.radioMenuPrefix = value return self end function VeafCombatZone:setBriefing(value) self.briefing = value return self end function VeafCombatZone:getBriefing() return self.briefing end function VeafCombatZone:setMissionEditorZoneName(value) self.missionEditorZoneName = value return self end function VeafCombatZone:getMissionEditorZoneName() return self.missionEditorZoneName end function VeafCombatZone:isActive() return self.active end function VeafCombatZone:setActive(value) self.active = value return self end function VeafCombatZone:isTraining() return self.training end function VeafCombatZone:setTraining(value) self.training = value if value then self.showUnitsList = true self.showZonePositionInfo = true end return self end function VeafCombatZone:isShowUnitsList() return self.showUnitsList end function VeafCombatZone:setShowUnitsList(value) self.showUnitsList = value return self end function VeafCombatZone:isShowZonePositionInfo() return self.showZonePositionInfo end function VeafCombatZone:setShowZonePositionInfo(value) self.showZonePositionInfo = value return self end function VeafCombatZone:isCompletable() return self.completable end function VeafCombatZone:setCompletable(value) self.completable = value return self end function VeafCombatZone:getTriggerZone() return self.triggerZone end function VeafCombatZone:getCenter() return self.zoneCenter end function VeafCombatZone:setRadioParentPath(value) self.radioParentPath = value return self end function VeafCombatZone:getRadioParentPath() return self.radioParentPath end function VeafCombatZone:setRadioGroupName(value) self.radioGroupName = value return self end function VeafCombatZone:getRadioGroupName() return self.radioGroupName end function VeafCombatZone:addSpawnedGroup(groupOrName) local groupName = groupOrName if type(groupName) ~= "string" then groupName = tostring(groupName) end veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:addSpawnedGroup(%s)",veaf.p(self.missionEditorZoneName), veaf.p(groupName))) if not self.spawnedGroups then self.spawnedGroups = {} end table.insert(self.spawnedGroups, groupName) return self end function VeafCombatZone:getSpawnedGroups() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getSpawnedGroups()",veaf.p(self.missionEditorZoneName))) veaf.loggers.get(veafCombatZone.Id):trace(veaf.serialize("self.spawnedGroups", self.spawnedGroups)) return self.spawnedGroups end function VeafCombatZone:clearSpawnedGroups() self.spawnedGroups = {} return self end function VeafCombatZone:addDelayedSpawner(id) veaf.loggers.get(veafCombatZone.Id):trace("VeafCombatZone[%s]:addDelayedSpawner(%s)", veaf.p(self.missionEditorZoneName), veaf.p(id)) if not self.delayedSpawners then self.delayedSpawners = {} end table.insert(self.delayedSpawners, id) return self end function VeafCombatZone:getDelayedSpawners() veaf.loggers.get(veafCombatZone.Id):trace("VeafCombatZone[%s]:getDelayedSpawners()", veaf.p(self.missionEditorZoneName)) veaf.loggers.get(veafCombatZone.Id):trace("self.delayedSpawners=%s", self.delayedSpawners) return self.delayedSpawners end function VeafCombatZone:clearDelayedSpawners() self.delayedSpawners = {} return self end function VeafCombatZone:addZoneElement(element) veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:addZoneElement(%s)",veaf.p(self.missionEditorZoneName), veaf.p(element:getName()))) if not self.elements then self.elements = {} end if not self.elementGroups then self.elementGroups = {} end table.insert(self.elements, element) if not self.elementGroups[element:getSpawnGroup()] then local elementGroup = {} elementGroup.spawnGroup = element:getSpawnGroup() elementGroup.spawnCount = element:getSpawnCount() elementGroup.elements = {} self.elementGroups[element:getSpawnGroup()] = elementGroup end local elementGroup = self.elementGroups[element:getSpawnGroup()] table.insert(elementGroup.elements, element) return self end function VeafCombatZone:addZoneElementsFromZoneNamed(zoneName) veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:addZoneElementsFromZoneNamed(%s)",veaf.p(self.missionEditorZoneName), veaf.p(zoneName))) if not zoneName then return self end local zone = veafCombatZone.GetZone(zoneName) if not zone then return self end local elements = zone:getZoneElements() if not elements then return self end for _, element in pairs(elements) do self:addZoneElement(element) end return self end function VeafCombatZone:getZoneElements() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getZoneElement()",veaf.p(self.missionEditorZoneName))) veaf.loggers.get(veafCombatZone.Id):trace(veaf.serialize("self.elements", self.elements)) return self.elements end function VeafCombatZone:getZoneElementsGroups() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getZoneElementsGroups()",veaf.p(self.missionEditorZoneName))) return self.elementGroups end -- get the list of chained combat zones; this is are list of combat zones, that are activated randomly function VeafCombatZone:getChainedCombatZones() if not self.chainedCombatZones then veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getChainedCombatZones() - Initializing",veaf.p(self.missionEditorZoneName))) self.chainedCombatZones = {} end veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getChainedCombatZones() = %s",veaf.p(self.missionEditorZoneName), veaf.p(self.chainedCombatZones))) return self.chainedCombatZones end -- add a chained combat zone (by name); the zone does not have to exist at the time the function is called function VeafCombatZone:addChainedCombatZone(combatZoneName) veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:addChainedCombatZone([%s])",veaf.p(self.missionEditorZoneName), veaf.p(combatZoneName))) table.insert(self:getChainedCombatZones(), combatZoneName) return self end -- get the next chained combat zone; if the list is more than 1 zone long, get one at random function VeafCombatZone:getNextChainedCombatZone() local nextZoneName = veaf.randomlyChooseFrom(self:getChainedCombatZones()) veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getNextChainedCombatZone() = [%s]",veaf.p(self.missionEditorZoneName), veaf.p(nextZoneName))) return nextZoneName end -- get the delay (in seconds) between the end of this combat zone and the start of the other; if the set value was a randomizable numeric, randomize it function VeafCombatZone:getChainedCombatZonesDelay() return veaf.getRandomizableNumeric(self.chainedCombatZonesDelay or 0) end -- set the delay (in seconds) between the end of this combat zone and the start of the other; can be a randomizable numeric (e.g. "1-5") function VeafCombatZone:setChainedCombatZonesDelay(value) veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:setChainedCombatZonesDelay([%s])",veaf.p(self.missionEditorZoneName), veaf.p(value))) if not value then value = 0 end self.chainedCombatZonesDelay = value return self end --- --- other methods --- function VeafCombatZone:scheduleWatchdogFunction() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:scheduleWatchdogFunction()",veaf.p(self.missionEditorZoneName))) if self:isCompletable() then self.watchdogFunctionId = mist.scheduleFunction(veafCombatZone.CompletionCheck,{self.missionEditorZoneName},timer.getTime()+veafCombatZone.SecondsBetweenWatchdogChecks) end return self end function VeafCombatZone:unscheduleWatchdogFunction() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:unscheduleWatchdogFunction()",veaf.p(self.missionEditorZoneName))) if self.watchdogFunctionId then mist.removeFunction(self.watchdogFunctionId) end self.watchdogFunctionId = nil return self end function VeafCombatZone:addObjective(value) table.insert(self.objectives, value) return self end function VeafCombatZone:addDefaultObjectives() -- TODO return self end function VeafCombatZone:initialize() veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatZone[%s]:initialize()",veaf.p(self.missionEditorZoneName))) -- check parameters if not self.missionEditorZoneName then return self else self.triggerZone = veaf.getTriggerZone(self.missionEditorZoneName) if not self.triggerZone then local message = string.format("Trigger zone [%s] does not exist in the mission !",veaf.p(self.missionEditorZoneName)) veaf.loggers.get(veafCombatZone.Id):error(message) trigger.action.outText(message,5) return self end end if not self.friendlyName then self:setFriendlyName(self.missionEditorZoneName) end if #self.objectives == 0 then self:addDefaultObjectives() end -- find the trigger zone center self.zoneCenter = mist.utils.zoneToVec3(self.missionEditorZoneName) if not self.zoneCenter then local message = string.format("Trigger zone [%s] does not exist in the mission !",veaf.p(self.missionEditorZoneName)) veaf.loggers.get(veafCombatZone.Id):error(message) trigger.action.outText(message,5) return self end veaf.loggers.get(veafCombatZone.Id):trace(string.format("zone center = [%s]",veaf.vecToString(self.zoneCenter))) -- find units in the trigger zone local units units, _ = veaf.safeUnpack(self:findUnitsInCombatZone()) -- process special commands in the units local alreadyAddedGroups = {} for _,unit in pairs(units) do local zoneElement = VeafCombatZoneElement:new() zoneElement:setCoalition(unit:getCoalition()) local unitName = unit:getName() veaf.loggers.get(veafCombatZone.Id):trace(string.format("processing unit [%s] of coalition [%d]", unitName, unit:getCoalition())) zoneElement:setPosition(unit:getPosition().p) local spawnRadius, command, spawnChance, spawnGroup, spawnCount, spawnDelay _, _, spawnRadius = unitName:lower():find("#spawnradius%s*=%s*(%d+)") _, _, command = unitName:lower():find("#command%s*=%s*\"([^\"]+)\"") _, _, spawnChance = unitName:lower():find("#spawnchance%s*=%s*(%d+)") _, _, spawnGroup = unitName:lower():find("#spawngroup%s*=%s*\"([^\"]+)\"") _, _, spawnCount = unitName:lower():find("#spawncount%s*=%s*(%d+)") _, _, spawnDelay = unitName:lower():find("#spawndelay%s*=%s*(%d+)") if spawnRadius then veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnRadius = [%d]", spawnRadius)) zoneElement:setSpawnRadius(spawnRadius) end if spawnChance then veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnChance = [%d]", spawnChance)) zoneElement:setSpawnChance(spawnChance) end if spawnCount then veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnCount = [%d]", spawnCount)) zoneElement:setSpawnCount(spawnCount) end if spawnGroup then veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnGroup = [%s]", spawnGroup)) zoneElement:setSpawnGroup(spawnGroup) end if spawnDelay then veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnDelay = [%s]", spawnDelay)) zoneElement:setSpawnDelay(spawnDelay) end if command then -- it's a fake unit transporting a VEAF command veaf.loggers.get(veafCombatZone.Id):trace(string.format("command = [%s]", command)) command = command .. ", czName " .. self:getMissionEditorZoneName() -- add the combat zone name to the command zoneElement:setVeafCommand(command) local groupName = unit:getGroup():getName() zoneElement:setName(groupName) veaf.loggers.get(veafCombatZone.Id):trace(string.format("groupName = [%s]", groupName)) local route = mist.getGroupRoute(groupName, 'task') zoneElement:setRoute(route) if not zoneElement:getSpawnGroup() then zoneElement:setSpawnGroup(groupName) end -- default the spawn group to the group name in case there is no spawn group defined else -- it's a group or a static unit local groupName = nil local objectCategory = Object.getCategory(unit) veaf.loggers.get(veafCombatZone.Id):trace("objectCategory=%s", veaf.p(objectCategory)) if objectCategory == 1 then local unitCategory = Unit.getCategory(unit) veaf.loggers.get(veafCombatZone.Id):trace("unitCategory=%s", veaf.p(unitCategory)) end if objectCategory == 3 or objectCategory == 6 then -- 3 is static objects, 6 is cargo (a kind of static object) groupName = unitName -- default for static objects = groups themselves zoneElement:setDcsStatic(true) if not zoneElement:getSpawnRadius() then zoneElement:setSpawnRadius(veafCombatZone.DefaultSpawnRadiusForStatics) end else groupName = unit:getGroup():getName() zoneElement:setDcsGroup(true) if not zoneElement:getSpawnRadius() then zoneElement:setSpawnRadius(veafCombatZone.DefaultSpawnRadiusForUnits) end end if not zoneElement:getSpawnGroup() then zoneElement:setSpawnGroup(groupName) end -- default the spawn group to the group name in case there is no spawn group defined if not alreadyAddedGroups[groupName] then -- add a group element veaf.loggers.get(veafCombatZone.Id):trace(string.format("adding group [%s]", groupName)) alreadyAddedGroups[groupName] = groupName zoneElement:setName(groupName) else veaf.loggers.get(veafCombatZone.Id):trace(string.format("skipping group [%s]", groupName)) zoneElement = nil -- don't add this element, it's a group that has already been added end end if zoneElement then self:addZoneElement(zoneElement) end end -- deactivate the zone veaf.loggers.get(veafCombatZone.Id):trace("desactivate the zone") self:desactivate() -- remove all units in the trigger zone (we want it CLEAN !) local _, groupNames = veaf.safeUnpack(self:findUnitsInCombatZone()) if (groupNames) then for _, groupName in pairs(groupNames) do veaf.loggers.get(veafCombatZone.Id):trace(string.format("destroying group [%s]",groupName)) local group = Group.getByName(groupName) if not group then group = StaticObject.getByName(groupName) end if group then group:destroy() end end end return self end function VeafCombatZone:getInformation() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getInformation()",veaf.p(self.missionEditorZoneName))) local message = "COMBAT ZONE "..self:getFriendlyName().." \n\n" if (self:getBriefing()) then message = message .. "BRIEFING: \n" message = message .. self:getBriefing() message = message .. "\n\n" end if self:isActive() then -- generate information dispatch local nbShipsR = 0 local nbVehiclesR = 0 local nbInfantryR = 0 local nbStaticsR = 0 local nbShipsB = 0 local nbVehiclesB = 0 local nbInfantryB = 0 local nbStaticsB = 0 local unitsByTypeR = {} local unitsByTypeB = {} for _, groupName in pairs(self:getSpawnedGroups()) do local group = Group.getByName(groupName) if group then for _, u in pairs(group:getUnits()) do local coa = u:getCoalition() if Object.getCategory(u) == 3 then if coa == 1 then nbStaticsR = nbStaticsR + 1 elseif coa == 2 then nbStaticsB = nbStaticsB + 1 end else local typeName = u:getTypeName() if typeName then local unit = veafUnits.findUnit(typeName) if unit then if coa == 1 then if not(unitsByTypeR[typeName]) then unitsByTypeR[typeName] = 0 end unitsByTypeR[typeName] = unitsByTypeR[typeName] + 1 if unit.vehicle then nbVehiclesR = nbVehiclesR + 1 elseif unit.naval then nbShipsR = nbShipsR + 1 else nbInfantryR = nbInfantryR + 1 end elseif coa == 2 then if not(unitsByTypeB[typeName]) then unitsByTypeB[typeName] = 0 end unitsByTypeB[typeName] = unitsByTypeB[typeName] + 1 if unit.vehicle then nbVehiclesB = nbVehiclesB + 1 elseif unit.naval then nbShipsB = nbShipsB + 1 else nbInfantryB = nbInfantryB + 1 end end end end end end end end if nbShipsB+nbStaticsB+nbVehiclesB+nbInfantryB > 0 and self:isShowUnitsList() then local msgs = {} if nbShipsB > 0 then table.insert(msgs, nbShipsB .. " ship(s)") end if nbStaticsB > 0 then table.insert(msgs, nbStaticsB .. " structure(s)") end if nbVehiclesB > 0 then table.insert(msgs, nbVehiclesB .. " vehicle(s)") end if nbInfantryB > 0 then table.insert(msgs, nbInfantryB .. " soldier(s)") end message = message .. "FRIENDS: ".. table.concat(msgs, ",") .." remaining.\n" if self:isTraining() then local firstUnit = true for name, count in pairs(unitsByTypeB) do local separator = ", " if firstUnit then separator = "" firstUnit = false end message = message .. string.format("%s%d %s",separator, count, name) end message = message .. "\n" end end if nbShipsR+nbStaticsR+nbVehiclesR+nbInfantryR > 0 and self:isShowUnitsList() then local msgs = {} if nbShipsR > 0 then table.insert(msgs, nbShipsR .. " ship(s)") end if nbStaticsR > 0 then table.insert(msgs, nbStaticsR .. " structure(s)") end if nbVehiclesR > 0 then table.insert(msgs, nbVehiclesR .. " vehicle(s)") end if nbInfantryR > 0 then table.insert(msgs, nbInfantryR .. " soldier(s)") end message = message .. "ENEMIES: ".. table.concat(msgs, ",") .." remaining.\n" if self:isTraining() then local firstUnit = true for name, count in pairs(unitsByTypeR) do local separator = ", " if firstUnit then separator = "" firstUnit = false end message = message .. string.format("%s%d %s",separator, count, name) end message = message .. "\n" end end message = message .. "\n" if self:isShowZonePositionInfo() then -- add coordinates and position from bullseye local zoneCenter = self:getCenter() local lat, lon = coord.LOtoLL(zoneCenter) local mgrsString = mist.tostringMGRS(coord.LLtoMGRS(lat, lon), 3) local bullseye = mist.utils.makeVec3(mist.DBs.missionData.bullseye.blue, 0) local vec = {x = zoneCenter.x - bullseye.x, y = zoneCenter.y - bullseye.y, z = zoneCenter.z - bullseye.z} local dir = mist.utils.round(mist.utils.toDegree(mist.utils.getDir(vec, bullseye)), 0) local dist = mist.utils.get2DDist(zoneCenter, bullseye) local distMetric = mist.utils.round(dist/1000, 0) local distImperial = mist.utils.round(mist.utils.metersToNM(dist), 0) local fromBullseye = string.format('%03d', dir) .. ' for ' .. distMetric .. 'km /' .. distImperial .. 'nm' message = message .. "LAT LON (decimal): " .. mist.tostringLL(lat, lon, 2) .. ".\n" message = message .. "LAT LON (DMS) : " .. mist.tostringLL(lat, lon, 0, true) .. ".\n" message = message .. "MGRS/UTM : " .. mgrsString .. ".\n" message = message .. "FROM BULLSEYE : " .. fromBullseye .. ".\n" message = message .. "\n" -- get altitude, qfe and wind information message = message .. veaf.weatherReport(zoneCenter, nil, true) end else message = message .. "zone is not yet active." end return message end function VeafCombatZone:spawnElement(zoneElement, now) veaf.loggers.get(veafCombatZone.Id):debug("VeafCombatZone[%s]:spawnElement([%s], [%s])",veaf.p(self:getFriendlyName()), veaf.p(zoneElement:getName()), veaf.p(now)) veaf.loggers.get(veafCombatZone.Id):trace("zoneElement=%s", zoneElement) if not now and zoneElement:getSpawnDelay() and type(zoneElement:getSpawnDelay()) == "number" then -- self-schedule veaf.loggers.get(veafCombatZone.Id):trace("scheduling spawn of zoneElement=%s in %s seconds", zoneElement:getName(), zoneElement:getSpawnDelay()) local id = mist.scheduleFunction(VeafCombatZone.spawnElement,{self, zoneElement, true}, timer.getTime()+zoneElement:getSpawnDelay()) self:addDelayedSpawner(id) else -- spawn now veaf.loggers.get(veafCombatZone.Id):trace("spawning zoneElement=%s now", zoneElement:getName()) local position = zoneElement:getPosition() if zoneElement:getSpawnRadius() > 0 then veaf.loggers.get(veafCombatZone.Id):trace(string.format("position=[%s]",veaf.vecToString(position))) veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnRadius=[%s]",zoneElement:getSpawnRadius())) local mistP = mist.getRandPointInCircle(position, zoneElement:getSpawnRadius()) veaf.loggers.get(veafCombatZone.Id):trace(string.format("mistP=[%s]",veaf.vecToString(mistP))) position = {x = mistP.x, y = position.y, z = mistP.y} end if zoneElement:isDcsStatic() or zoneElement:isDcsGroup() then veaf.loggers.get(veafCombatZone.Id):trace(string.format("respawning group [%s] at position [%s]",zoneElement:getName(), veaf.vecToString(position))) local vars = {} vars.gpName = zoneElement:getName() vars.name = zoneElement:getName() vars.newGroupName = veaf.getNameForSpawnedGroup(zoneElement:getCoalition(), zoneElement:getName(), self:getMissionEditorZoneName()) vars.route = zoneElement:getRoute() vars.action = 'respawn' vars.point = position vars.renameUnitsSequentially = true local newGroup = mist.teleportToPoint(vars) if type(newGroup) == 'table' then veaf.loggers.get(veafCombatZone.Id):trace(string.format("[%s]:activate() - mist.teleportToPoint([%s])", self:getMissionEditorZoneName(), zoneElement:getName())) self:addSpawnedGroup(newGroup.name) veaf.readyForCombat(newGroup.name) else veaf.loggers.get(veafCombatZone.Id):trace(string.format("[%s]:activate() - mist.teleportToPoint([%s]) failed", self:getMissionEditorZoneName(), zoneElement:getName())) end elseif zoneElement:getVeafCommand() then veaf.loggers.get(veafCombatZone.Id):trace(string.format("executing command [%s] at position [%s]",zoneElement:getVeafCommand(), veaf.vecToString(position))) local spawnedGroups = {} veafInterpreter.execute(zoneElement:getVeafCommand(), position, zoneElement:getCoalition(), nil, spawnedGroups) for _, newGroup in pairs(spawnedGroups) do veaf.loggers.get(veafCombatZone.Id):trace(string.format("[%s].addSpawnedGroup", zoneElement:getName())) self:addSpawnedGroup(newGroup) veaf.loggers.get(veafCombatZone.Id):trace(string.format("newGroup = [%s]", newGroup)) local route = zoneElement:getRoute() veaf.loggers.get(veafCombatZone.Id):trace(string.format("got route")) local result = mist.goRoute(newGroup, route) veaf.loggers.get(veafCombatZone.Id):trace(string.format("sent group on its way")) end end end end -- activate the zone function VeafCombatZone:activate() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:activate()",self:getMissionEditorZoneName())) self:setActive(true) for _, zoneElementGroup in pairs(self:getZoneElementsGroups()) do veaf.loggers.get(veafCombatZone.Id):trace(string.format("processing spawnGroup [%s]",zoneElementGroup.spawnGroup)) local spawnCount = zoneElementGroup.spawnCount veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnCount = [%d]",spawnCount)) local tries = 10 local alreadySpawnedElements = {} local shuffledIndexes = {} for i=1,#zoneElementGroup.elements do local zoneElement = zoneElementGroup.elements[i] alreadySpawnedElements[zoneElement:getName()]=false table.insert(shuffledIndexes, i) end veaf.shuffle(shuffledIndexes) while spawnCount > 0 and tries > 0 do veaf.loggers.get(veafCombatZone.Id):trace(string.format("tries = [%d]",tries)) tries = tries - 1 for i=1,#shuffledIndexes do local zoneElement = zoneElementGroup.elements[shuffledIndexes[i]] veaf.loggers.get(veafCombatZone.Id):trace(string.format("processing element [%s]",veaf.p(zoneElement))) if spawnCount > 0 then if not alreadySpawnedElements[zoneElement:getName()] then veaf.loggers.get(veafCombatZone.Id):trace(string.format("processing element [%s]",zoneElement:getName())) local chance = math.random(0, 100) if tries == 1 then chance = 0 end -- force chance if in the last try veaf.loggers.get(veafCombatZone.Id):trace(string.format("chance = [%d]",chance)) veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnChance = [%d]",zoneElement:getSpawnChance())) if chance <= zoneElement:getSpawnChance() then veaf.loggers.get(veafCombatZone.Id):trace(string.format("chance hit (%d <= %d)",chance, zoneElement:getSpawnChance())) spawnCount = spawnCount - 1 alreadySpawnedElements[zoneElement:getName()]=true self:spawnElement(zoneElement) else veaf.loggers.get(veafCombatZone.Id):trace(string.format("chance missed (%d > %d)",chance, zoneElement:getSpawnChance())) end else veaf.loggers.get(veafCombatZone.Id):trace(string.format("already spawned [%s]",zoneElement:getName())) end end end end end -- start the completion watchdog self:scheduleWatchdogFunction() -- refresh the radio menu self:updateRadioMenu() return self end -- activate the next chained zone (if any) function VeafCombatZone:activateNextChainedZone() local nextZoneName = self:getNextChainedCombatZone() local nextZone = veafCombatZone.GetZone(nextZoneName) if not nextZone then return self end local delay = self:getChainedCombatZonesDelay() veaf.loggers.get(veafCombatZone.Id):trace(string.format("activating the next chained zone ([%s]) in %s seconds)",veaf.p(nextZoneName), veaf.p(delay))) mist.scheduleFunction(VeafCombatZone.activate,{nextZone},timer.getTime()+delay) return self end -- desactivate the zone function VeafCombatZone:desactivate() veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatZone[%s]:desactivate()",veaf.p(self.missionEditorZoneName))) self:setActive(false) self:unscheduleWatchdogFunction() for _, delayedSpawner in pairs(self:getDelayedSpawners()) do veaf.loggers.get(veafCombatZone.Id):trace("unscheduling delayed spawner %s", delayedSpawner) mist.removeFunction(delayedSpawner) end self:clearDelayedSpawners() for _, groupName in pairs(self:getSpawnedGroups()) do veaf.loggers.get(veafCombatZone.Id):trace(string.format("trying to destroy group [%s]",groupName)) local group = Group.getByName(groupName) if not group then group = StaticObject.getByName(groupName) if group then veaf.loggers.get(veafCombatZone.Id):trace(string.format("found static [%s]",group:getName())) else veaf.loggers.get(veafCombatZone.Id):info(string.format("cannot find static [%s]",groupName)) end end if group then veaf.loggers.get(veafCombatZone.Id):trace(string.format("destroying group [%s]",group:getName())) group:destroy() end end self:clearSpawnedGroups() if self.enableJunkCleanup then -- remove the junk that the battle left behind veaf.loggers.get(veafCombatZone.Id):trace("removing the junk that the battle left behind") local zone = veaf.getTriggerZone(self.missionEditorZoneName) local volS = { id = world.VolumeType.SPHERE, params = {point = veaf.placePointOnLand(zone), radius = zone.radius} } veaf.loggers.get(veafCombatZone.Id):trace(string.format("volS=%s",veaf.p(volS))) local n=world.removeJunk(volS) veaf.loggers.get(veafCombatZone.Id):trace(string.format("world.removeJunk() returned %s",veaf.p(n))) end -- refresh the radio menu self:updateRadioMenu() return self end -- check if there are still units in zone function VeafCombatZone:completionCheck() veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatZone[%s]:completionCheck()",veaf.p(self.missionEditorZoneName))) if not self:isCompletable() then return end local nbUnitsR = 0 local nbUnitsB = 0 for _, groupName in pairs(self:getSpawnedGroups()) do local group = Group.getByName(groupName) if group then for _, unit in pairs(group:getUnits()) do local coa = unit:getCoalition() if coa == 1 then nbUnitsR = nbUnitsR + 1 elseif coa == 2 then nbUnitsB = nbUnitsB + 1 end end else local static = StaticObject.getByName(groupName) if static then local coa = static:getCoalition() if coa == 1 then nbUnitsR = nbUnitsR + 1 elseif coa == 2 then nbUnitsB = nbUnitsB + 1 end end end end veaf.loggers.get(veafCombatZone.Id):trace(string.format("nbUnitsB=%d",nbUnitsB)) veaf.loggers.get(veafCombatZone.Id):trace(string.format("nbUnitsR=%d",nbUnitsR)) if nbUnitsR == 0 then -- everyone is dead, let's end this mess if veafCombatZone.EventMessages.CombatZoneComplete then local message = string.format(veafCombatZone.EventMessages.CombatZoneComplete, self:getFriendlyName()) trigger.action.outText(message, 15) end -- call the onCompleted hook if self.onCompletedHook then self.onCompletedHook(self) end -- desactivate the zone self:desactivate() -- activate the next chained zone if needed self:activateNextChainedZone() else -- reschedule self:scheduleWatchdogFunction() end end -- pop a smoke marker over the zone function VeafCombatZone:popSmoke() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:popSmoke()",veaf.p(self.missionEditorZoneName))) veaf.loggers.get(veafCombatZone.Id):trace(string.format("self:getCenter()=%s",veaf.vecToString(self:getCenter()))) local smokePoint = self:getCenter() if self:isTraining() then -- compute the barycenter of all remaining units local totalPosition = {x = 0,y = 0,z = 0} local units, _ = veaf.safeUnpack(self:findUnitsInCombatZone()) for count = 1,#units do if units[count] then totalPosition = mist.vec.add(totalPosition,Unit.getPosition(units[count]).p) end end if #units > 0 then smokePoint = mist.vec.scalar_mult(totalPosition,1/#units) end end veaf.loggers.get(veafCombatZone.Id):trace(string.format("smokePoint=%s",veaf.vecToString(smokePoint))) veafSpawn.spawnSmoke(smokePoint, trigger.smokeColor.Red) self.smokeResetFunctionId = mist.scheduleFunction(veafCombatZone.SmokeReset,{self.missionEditorZoneName},timer.getTime()+veafCombatZone.SecondsBetweenSmokeRequests) trigger.action.outText(string.format(veafCombatZone.EventMessages.PopSmokeRequest, self:getFriendlyName()),5) self:updateRadioMenu() return self end -- pop an illumination flare over a zone function VeafCombatZone:popFlare() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:popFlare()",veaf.p(self.missionEditorZoneName))) veaf.loggers.get(veafCombatZone.Id):trace(string.format("self:getCenter()=%s",veaf.vecToString(self:getCenter()))) veafSpawn.spawnIlluminationFlare(self:getCenter()) self.flareResetFunctionId = mist.scheduleFunction(veafCombatZone.FlareReset,{self.missionEditorZoneName},timer.getTime()+veafCombatZone.SecondsBetweenFlareRequests) trigger.action.outText(string.format(veafCombatZone.EventMessages.UseFlareRequest, self:getFriendlyName()),5) self:updateRadioMenu() return self end -- updates the radio menu according to the zone state function VeafCombatZone:updateRadioMenu(inBatch) veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatZone[%s]:updateRadioMenu(%s)",veaf.p(self.missionEditorZoneName), tostring(inBatch))) veaf.loggers.get(veafCombatZone.Id):debug("radioGroupName=%s", self.radioGroupName) -- do not update the radio menu if not yet initialized or if we don't want to if not self.radioParentPath or not self.enableRadioMenu then return self end local shouldAddSubMenu = self.enableUserActivation or self.active veaf.loggers.get(veafCombatZone.Id):debug("User activation enabled : %s, Zone active: %s, shouldAddSubMenu: %s", veaf.p(self.enableUserActivation), veaf.p(self.active), veaf.p(shouldAddSubMenu)) -- reset the radio menu if self.radioRootPath then veaf.loggers.get(veafCombatZone.Id):debug("Remove the radio submenu %s", veaf.p(self:getRadioMenuName())) veafRadio.delSubmenu(self:getRadioMenuName(), self.radioParentPath) veafRadio.delSubmenu(self:getRadioMenuName(true), self.radioParentPath) self.radioRootPath = nil end if shouldAddSubMenu then veaf.loggers.get(veafCombatZone.Id):debug("add the radio submenu") self.radioRootPath = veafRadio.addSubMenu(self:getRadioMenuName(self:isActive()), self.radioParentPath) end if shouldAddSubMenu then -- populate the radio menu veaf.loggers.get(veafCombatZone.Id):debug("populate the radio menu") -- global commands veafRadio.addCommandToSubmenu("Get info", self.radioRootPath, veafCombatZone.GetInformationOnZone, self.missionEditorZoneName, veafRadio.USAGE_ForGroup) if self:isActive() then -- zone is active, set up accordingly (desactivate zone, get information, pop smoke, etc.) veaf.loggers.get(veafCombatZone.Id):debug("zone is active") if(self.enableUserActivation) then if self:isTraining() then veafRadio.addCommandToSubmenu('Desactivate zone', self.radioRootPath, veafCombatZone.DesactivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll) else veafRadio.addSecuredCommandToSubmenu('Desactivate zone', self.radioRootPath, veafCombatZone.DesactivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll) end end if self.enableSmokeAndFlare then if self.smokeResetFunctionId then veafRadio.addCommandToSubmenu('Smoke not available', self.radioRootPath, veaf.emptyFunction, nil, veafRadio.USAGE_ForAll) else veafRadio.addCommandToSubmenu('Request RED smoke on target', self.radioRootPath, veafCombatZone.SmokeZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll) end if self.flareResetFunctionId then veafRadio.addCommandToSubmenu('Flare not available', self.radioRootPath, veaf.emptyFunction, nil, veafRadio.USAGE_ForAll) else veafRadio.addCommandToSubmenu('Request illumination flare on target', self.radioRootPath, veafCombatZone.LightUpZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll) end end else -- zone is not active, set up accordingly (activate zone) veaf.loggers.get(veafCombatZone.Id):debug("zone is not active") if self.enableUserActivation then if self:isTraining() then veafRadio.addCommandToSubmenu('Activate zone', self.radioRootPath, veafCombatZone.ActivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll) else veafRadio.addSecuredCommandToSubmenu('Activate zone', self.radioRootPath, veafCombatZone.ActivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll) end end end end if not inBatch then veafRadio.refreshRadioMenu() end return self end --- --- lists all units and statics (and their groups names) in a combat zone that also match the combat zone name --- function VeafCombatZone:findUnitsInCombatZone() local unitsNames = veaf.getUnitsNamesOfCoalition(true, nil) -- include statics, all coalitions local units = {} local resultUnits = {} local groupNames = {} local alreadyAddedGroups = {} local triggerZone = self:getTriggerZone() local upperTriggerzoneName = self:getMissionEditorZoneName():upper() if not triggerZone then return self end veaf.loggers.get(veafCombatZone.Id):trace("#unitsNames=%s", veaf.p(#unitsNames)) veaf.loggers.get(veafCombatZone.Id):trace("triggerZone.type=%s", veaf.p(triggerZone.type)) if triggerZone.type == 0 then -- circular units = mist.getUnitsInZones(unitsNames, {self:getMissionEditorZoneName()}) elseif triggerZone.type == 2 then -- quad point units = mist.getUnitsInPolygon(unitsNames, triggerZone.verticies) end veaf.loggers.get(veafCombatZone.Id):trace("#units=%s", veaf.p(#units)) for _, unit in pairs(units) do local unitName = unit:getName() local objectCategory = Object.getCategory(unit) local groupName = nil veaf.loggers.get(veafCombatZone.Id):trace(string.format("processing unit [%s]", unitName)) veaf.loggers.get(veafCombatZone.Id):trace(string.format("objectCategory = [%d]", objectCategory)) if objectCategory == 3 or objectCategory == 6 then -- 3 is static objects, 6 is cargo (a kind of static object) groupName = unitName -- default for static objects = groups themselves else groupName = unit:getGroup():getName() end veaf.loggers.get(veafCombatZone.Id):trace(string.format("groupName = %s", groupName)) if string.sub(groupName:upper(),1,string.len(upperTriggerzoneName))==upperTriggerzoneName then resultUnits[#resultUnits + 1] = unit if not alreadyAddedGroups[groupName] then alreadyAddedGroups[groupName] = groupName groupNames[#groupNames + 1] = groupName end end end veaf.loggers.get(veafCombatZone.Id):trace(string.format("found %d units (%d groups) in zone", #resultUnits, #groupNames)) return {resultUnits, groupNames} end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafCombatOperationTaskingOrder object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafCombatOperationTaskingOrder = { -- combat zone of the tasking order zone = nil, -- what tasking orders needs to be completed before starting this one requiredCompleteNames = {} } VeafCombatOperationTaskingOrder.__index = VeafCombatOperationTaskingOrder function VeafCombatOperationTaskingOrder:new(zone) local self = setmetatable({}, VeafCombatOperationTaskingOrder) self.zone = zone self.requiredCompleteNames = {} return self end function VeafCombatOperationTaskingOrder:setRequiredComplete(requiredCompleteNames) self.requiredCompleteNames = requiredCompleteNames return self end function VeafCombatOperationTaskingOrder:getZone() return self.zone end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafCombatOperation object ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafCombatOperation = VeafCombatZone:new() function VeafCombatOperation:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object -- operation name (human-friendly) objectToCreate.friendlyName = nil -- technical operation name (named missionEditorZoneName not to break all zone stuffs) objectToCreate.missionEditorZoneName = nil -- mission briefing objectToCreate.briefing = nil -- operation is active objectToCreate.active = false -- list of zones used as tasking order objectToCreate.taskingOrderList = {} -- dictionnary of zones used as tasking order objectToCreate.taskingOrderDict = {} -- combat zone that we want to be completed before continuing operation objectToCreate.primaryTaskingOrders = {} -- the watchdog function checks for zone objectives completion objectToCreate.watchdogFunctionId = nil -- function to call when combat zone is over. The function is passed self combat zone objectToCreate.onCompletedHook = nil -- how many tasks were complete so far objectToCreate.currentCompletedTaskingOrderCount = 0 return objectToCreate end --- --- setters and getters --- function VeafCombatOperation:setOnCompletedHook(onCompletedFunction) self.onCompletedHook = onCompletedFunction return self end function VeafCombatOperation:getRadioMenuName() return self:getFriendlyName() end function VeafCombatOperation:getInformation() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:getInformation()",veaf.p(self.missionEditorZoneName) )) local message = "OPERATION "..self:getFriendlyName().." \n\n" if (self:getBriefing()) then message = message .. messageSeparator message = message .. self:getBriefing() message = message .. "\n\n" end if self:isActive() then message = message .. messageSeparator .. "Air Tasking Orders: \n" for _, primaryTaskingOrder in pairs(self.primaryTaskingOrders) do if primaryTaskingOrder.zone:isActive() then message = message .. primaryTaskingOrder:getZone():getFriendlyName() .. "\n" end end else message = message .. string.format(veafCombatZone.EventMessages.CombatOperationComplete, self:getFriendlyName()) end return message end function VeafCombatOperation:addTaskingOrder(zone, requiredComplete) -- add requiredComplete in log veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:addTaskingOrder(%s)",veaf.p(self.missionEditorZoneName), veaf.p(zone.missionEditorZoneName))) veaf.loggers.get(veafCombatZone.Id):trace(string.format("Adding combat zone %s to operation %s", zone.missionEditorZoneName, veaf.p(self.missionEditorZoneName))) veaf.loggers.get(veafCombatZone.Id):trace(string.format("Tasks required before activation: %s", veaf.p(requiredComplete))) for _, mandatoryZoneName in pairs(requiredComplete or {}) do if(not self.taskingOrderDict[mandatoryZoneName]) then veaf.loggers.get(veafCombatZone.Id):error(string.format("Cannot add mandatory zone %s as it is not in known zones", veaf.p(mandatoryZoneName))) return self end end veaf.loggers.get(veafCombatZone.Id):trace("remove task order from combat zone radio menu") zone:disableRadioMenu() -- adds tasking order to the zone lists to make it accessible veafCombatZone.AddZone(zone) local newTaskingOrder = VeafCombatOperationTaskingOrder:new(zone) :setRequiredComplete(requiredComplete or {}) table.insert(self.taskingOrderList, newTaskingOrder) self.taskingOrderDict[zone.missionEditorZoneName] = newTaskingOrder return self end ------------------- --- Other methods ------------------- function VeafCombatOperation:scheduleWatchdogFunction() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:scheduleWatchdogFunction()", veaf.p(self.missionEditorZoneName))) self.watchdogFunctionId = mist.scheduleFunction(veafCombatZone.CompletionCheck,{self.missionEditorZoneName},timer.getTime()+veafCombatZone.SecondsBetweenWatchdogChecks) return self end function VeafCombatOperation:unscheduleWatchdogFunction() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:unscheduleWatchdogFunction()",veaf.p(self.missionEditorZoneName))) if self.watchdogFunctionId then mist.removeFunction(self.watchdogFunctionId) end self.watchdogFunctionId = nil return self end function VeafCombatOperation:updatePrimaryTasks() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:updatePrimaryTasks()",veaf.p(self.missionEditorZoneName))) veaf.loggers.get(veafCombatZone.Id):trace("Clear primary tasks") self.primaryTaskingOrders = {} veaf.loggers.get(veafCombatZone.Id):trace("Look for next tasks") local newPrimaryTasks = {} for _, candidateTaskingOrder in pairs(self.taskingOrderDict) do -- filter tasks that are not completed yet if candidateTaskingOrder:getZone():isActive() then local requirementFulfilled = true for _, requiredCombatZoneName in pairs(candidateTaskingOrder.requiredCompleteNames) do local requiredCombatZone = veafCombatZone.GetZone(requiredCombatZoneName) -- if any of required tasking order is active, then tasking order is not eligible if requiredCombatZone:isActive() then requirementFulfilled = false break end end if requirementFulfilled then table.insert(newPrimaryTasks, candidateTaskingOrder) end end end -- No task left, operation complete ! if veaf.length(newPrimaryTasks) == 0 then veaf.loggers.get(veafCombatZone.Id):trace("No tasks left") self:desactivate() if veafCombatZone.EventMessages.CombatOperationComplete then trigger.action.outText(string.format(veafCombatZone.EventMessages.CombatOperationComplete, self.friendlyName), 10) end return self end veaf.loggers.get(veafCombatZone.Id):trace("Setting new primary tasks") self.primaryTaskingOrders = newPrimaryTasks end -- checks if primary tasks are completed to unlock next function VeafCombatOperation:completionCheck() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:completionCheck()",veaf.p(self.missionEditorZoneName))) local completedTaskingOrderCount = 0 -- if any of primary tasks is still active, then check is done for _, primaryTask in pairs(self.primaryTaskingOrders) do if not primaryTask:getZone():isActive() then veaf.loggers.get(veafCombatZone.Id):trace(string.format("Primary task %s is completed",primaryTask:getZone():getFriendlyName())) completedTaskingOrderCount = completedTaskingOrderCount + 1 end end veaf.loggers.get(veafCombatZone.Id):trace(string.format("%s completed out of %s, previous was %s",completedTaskingOrderCount, #self.primaryTaskingOrders, self.currentCompletedTaskingOrderCount)) if completedTaskingOrderCount == #self.primaryTaskingOrders then veaf.loggers.get(veafCombatZone.Id):trace("Primary tasks complete") self:updatePrimaryTasks() self:updateRadioMenu() completedTaskingOrderCount = 0 if not self:isActive() then return self end end veaf.loggers.get(veafCombatZone.Id):trace("Still got work to do.") if completedTaskingOrderCount ~= self.currentCompletedTaskingOrderCount then veaf.loggers.get(veafCombatZone.Id):trace("New tasking order completed. Update radio.") self:updatePrimaryTasks() self:updateRadioMenu() end self.currentCompletedTaskingOrderCount = completedTaskingOrderCount -- reschedule self:scheduleWatchdogFunction() return self end function VeafCombatOperation:initialize() veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatOperation[%s]:initialize()",veaf.p(self.missionEditorZoneName))) -- check parameters if not self.missionEditorZoneName then return self end if not self.friendlyName then self:setFriendlyName(self.missionEditorZoneName) end -- initializes member combat zones and sets starting primary tasks for _, taskingOrder in pairs(self.taskingOrderDict) do taskingOrder:getZone():initialize() end -- deactivate the zone veaf.loggers.get(veafCombatZone.Id):trace("desactivate the operation") self:desactivate() return self end -- activate the operation function VeafCombatOperation:activate() veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:activate()", veaf.p(self.missionEditorZoneName))) self:setActive(true) local primaryTasks = {} -- activates member combat zones and sets starting primary tasks veaf.loggers.get(veafCombatZone.Id):trace("activate the operation's zones") for _, taskingOrder in pairs(self.taskingOrderDict) do taskingOrder:getZone():activate() -- selects combat zones with no requiredComplete combat zones if veaf.length(taskingOrder.requiredCompleteNames) == 0 then table.insert(primaryTasks, taskingOrder) end end veaf.loggers.get(veafCombatZone.Id):trace("set primary task") self.primaryTaskingOrders = primaryTasks -- schedule the watchdog function self:scheduleWatchdogFunction() -- refresh the radio menu self:updateRadioMenu() return self end -- desactivate the operation function VeafCombatOperation:desactivate() veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatOperation[%s]:desactivate()",veaf.p(self.missionEditorZoneName))) self:setActive(false) -- unscheduel watchdog function self:unscheduleWatchdogFunction() -- refresh the radio menu self:updateRadioMenu() return self end -- updates the radio menu according to the zone state function VeafCombatOperation:updateRadioMenu(inBatch) veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatOperation[%s]:updateRadioMenu(%s)",veaf.p(self.missionEditorZoneName), veaf.p(inBatch))) -- do not update the radio menu if not yet initialized if not veafCombatZone.rootPath then return self end local menuToFill = veafCombatZone.rootPath if(veafCombatZone.operationRootPath) then menuToFill = veafCombatZone.operationRootPath end -- reset the radio menu if self.radioRootPath then veaf.loggers.get(veafCombatZone.Id):trace("reset the radio submenu") veafRadio.clearSubmenu(self.radioRootPath) else veaf.loggers.get(veafCombatZone.Id):trace("add the radio submenu") self.radioRootPath = veafRadio.addSubMenu(self:getRadioMenuName(), menuToFill) end -- populate the radio menu veaf.loggers.get(veafCombatZone.Id):trace("populate the radio menu") -- global commands veafRadio.addCommandToSubmenu("Get info", self.radioRootPath, veafCombatZone.GetInformationOnZone, self.missionEditorZoneName, veafRadio.USAGE_ForGroup) for _, taskingOrder in pairs(self.primaryTaskingOrders) do if taskingOrder.zone:isActive() then veaf.loggers.get(veafCombatZone.Id):trace(string.format("Add briefing for %s, %s", taskingOrder.zone:getFriendlyName(), taskingOrder.zone:getMissionEditorZoneName())) veafRadio.addCommandToSubmenu("Briefing " .. taskingOrder.zone:getFriendlyName(), self.radioRootPath, veafCombatZone.GetInformationOnZone, taskingOrder.zone:getMissionEditorZoneName(), veafRadio.USAGE_ForGroup) else veaf.loggers.get(veafCombatZone.Id):trace(string.format("Skip briefing for %s, %s as it is not active", taskingOrder.zone:getFriendlyName(), taskingOrder.zone:getMissionEditorZoneName())) end end if self:isActive() then -- zone is active, set up accordingly (desactivate zone, get information, pop smoke, etc.) veaf.loggers.get(veafCombatZone.Id):trace("zone is active") -- veafRadio.addSecuredCommandToSubmenu('Desactivate zone', self.radioRootPath, veafCombatZone.DesactivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll) -- if self.smokeResetFunctionId then -- veafRadio.addCommandToSubmenu('Smoke not available', self.radioRootPath, veaf.emptyFunction, nil, veafRadio.USAGE_ForAll) -- else -- veafRadio.addCommandToSubmenu('Request RED smoke on target', self.radioRootPath, veafCombatZone.SmokeZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll) -- end -- if self.flareResetFunctionId then -- veafRadio.addCommandToSubmenu('Flare not available', self.radioRootPath, veaf.emptyFunction, nil, veafRadio.USAGE_ForAll) -- else -- veafRadio.addCommandToSubmenu('Request illumination flare on target', self.radioRootPath, veafCombatZone.LightUpZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll) -- end else -- zone is not active, set up accordingly (activate zone) veaf.loggers.get(veafCombatZone.Id):trace("zone is not active") -- veafRadio.addSecuredCommandToSubmenu('Activate zone', self.radioRootPath, veafCombatZone.ActivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll) end if not inBatch then veafRadio.refreshRadioMenu() end return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- global functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------- --- GLOBAL INTERFACE, working for both zones and operations -------------------------------------------------------------------------------------------------------------- function veafCombatZone.GetZone(zoneName) veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.GetZone([%s])", veaf.p(zoneName))) veaf.loggers.get(veafCombatZone.Id):trace(string.format("Searching for zone with name [%s]", veaf.p(zoneName))) if zoneName then local zone = veafCombatZone.zonesDict[zoneName:lower()] if not zone then local message = string.format("VeafCombatZone [%s] was not found !",zoneName) veaf.loggers.get(veafCombatZone.Id):error(message) trigger.action.outText(message,5) end return zone else return nil end end -- add a zone function veafCombatZone.AddZone(zone) veaf.loggers.get(veafCombatZone.Id):debug(string.format("veafCombatZone.AddZone([%s])",veaf.p(veaf.ifnns(zone, "missionEditorZoneName")))) if zone then zone:initialize() table.insert(veafCombatZone.zonesList, zone) veafCombatZone.zonesDict[zone.missionEditorZoneName:lower()] = zone return zone else return nil end end -- activate a zone by number function veafCombatZone.ActivateZoneNumber(number, silent) veaf.loggers.get(veafCombatZone.Id):debug(string.format("veafCombatZone.ActivateZoneNumber([%s])",veaf.p(number))) local zone = veafCombatZone.zonesList[number] if zone then veafCombatZone.ActivateZone(zone:getMissionEditorZoneName(), silent) end end -- activate a zone function veafCombatZone.ActivateZone(zoneName, silent) veaf.loggers.get(veafCombatZone.Id):debug(string.format("veafCombatZone.ActivateZone([%s])", veaf.p(zoneName))) local zone = veafCombatZone.GetZone(zoneName) if zone then if zone:isActive() then if not silent then trigger.action.outText("VeafCombatZone "..zone:getFriendlyName().." is already active.", 10) end return end mist.scheduleFunction(zone.activate,{zone},timer.getTime()+1) if not silent then trigger.action.outText("VeafCombatZone "..zone:getFriendlyName().." has been activated.", 10) mist.scheduleFunction(veafCombatZone.GetInformationOnZone,{{zoneName}},timer.getTime()+2) end return zone else return nil end end -- desactivate a zone by number function veafCombatZone.DesactivateZoneNumber(number, silent) veaf.loggers.get(veafCombatZone.Id):debug(string.format("veafCombatZone.DesactivateZoneNumber([%s])",veaf.p(number))) local zone = veafCombatZone.zonesList[number] if zone then veafCombatZone.DesactivateZone(zone:getMissionEditorZoneName(), silent) end end -- desactivate a zone by name function veafCombatZone.DesactivateZone(zoneName, silent) veaf.loggers.get(veafCombatZone.Id):debug(string.format("veafCombatZone.DesactivateZone([%s])", veaf.p(zoneName))) local zone = veafCombatZone.GetZone(zoneName) if zone then if not(zone:isActive()) then if not silent then trigger.action.outText("VeafCombatZone "..zone:getFriendlyName().." is not active.", 10) end return end zone:desactivate() if not silent then trigger.action.outText("VeafCombatZone "..zone:getFriendlyName().." has been desactivated.", 10) end return zone else return nil end end -- print information about a zone function veafCombatZone.GetInformationOnZone(parameters) veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.GetInformationOnZone([%s])",veaf.p(parameters))) local zoneName, unitName = veaf.safeUnpack(parameters) local zone = veafCombatZone.GetZone(zoneName) if zone then local text = zone:getInformation() if unitName then veaf.outTextForGroup(unitName, text, 30) else trigger.action.outText(text, 30) end return zone else return nil end end -- pop a smoke over a zone function veafCombatZone.SmokeZone(zoneName) veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.SmokeZone([%s])", veaf.p(zoneName))) local zone = veafCombatZone.GetZone(zoneName) if zone then zone:popSmoke() return zone else return nil end end -- pop an illumination flare over a zone function veafCombatZone.LightUpZone(zoneName) veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.LightUpZone([%s])", veaf.p(zoneName))) local zone = veafCombatZone.GetZone(zoneName) if zone then zone:popFlare() return zone else return nil end end -- reset the "pop smoke" menus function veafCombatZone.SmokeReset(zoneName) veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.SmokeReset([%s])", veaf.p(zoneName))) local zone = veafCombatZone.GetZone(zoneName) if zone then zone.smokeResetFunctionId = nil zone:updateRadioMenu() return zone else return nil end end -- reset the "pop flare" menus function veafCombatZone.FlareReset(zoneName) veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.FlareReset([%s])", veaf.p(zoneName))) local zone = veafCombatZone.GetZone(zoneName) if zone then zone.flareResetFunctionId = nil zone:updateRadioMenu() return zone else return nil end end -- call the completion watchdog methods function veafCombatZone.CompletionCheck(zoneName) veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.CompletionCheck([%s])", veaf.p(zoneName))) local zone = veafCombatZone.GetZone(zoneName) if zone then zone:completionCheck() return zone else return nil end end -------------------------------------------------------------------------------------------------------------- --- END OF GLOBAL INTERFACE -------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Build the initial radio menu function veafCombatZone.buildRadioMenu() veaf.loggers.get(veafCombatZone.Id):debug("buildRadioMenu()") -- don't create an empty menu if veaf.length(veafCombatZone.zonesDict) == 0 then return end veafCombatZone.rootPath = veafRadio.addMenu(veafCombatZone.RadioMenuName) veafCombatZone.combatZoneRootPath = veafCombatZone.rootPath if not(veafRadio.skipHelpMenus) then veafRadio.addCommandToSubmenu("HELP", veafCombatZone.rootPath, veafCombatZone.help, nil, veafRadio.USAGE_ForGroup) end if(veafCombatZone.CombatZoneRadioMenuName) then veafCombatZone.combatZoneRootPath = veafRadio.addSubMenu(veafCombatZone.CombatZoneRadioMenuName, veafCombatZone.rootPath) end if(veafCombatZone.OperationRadioMenuName) then veafCombatZone.operationRootPath = veafRadio.addSubMenu(veafCombatZone.OperationRadioMenuName, veafCombatZone.rootPath) end -- sort the zones alphabetically local names = {} local sortedZones = {} for _, zone in pairs(veafCombatZone.zonesDict) do table.insert(sortedZones, {name=zone:getMissionEditorZoneName(), sort=zone:getFriendlyName()}) end local function compare(a,b) if not(a) then a = {} end if not(a["sort"]) then a["sort"] = 0 end if not(b) then b = {} end if not(b["sort"]) then b["sort"] = 0 end return a["sort"] < b["sort"] end table.sort(sortedZones, compare) for i = 1, #sortedZones do table.insert(names, sortedZones[i].name) end veaf.loggers.get(veafCombatZone.Id):trace("veafCombatZone.buildRadioMenu() - dumping names") for i = 1, #names do veaf.loggers.get(veafCombatZone.Id):trace("veafCombatZone.buildRadioMenu().names -> " .. names[i]) end for _, zoneName in pairs(names) do local zone = veafCombatZone.GetZone(zoneName) if zone then if zone:getRadioGroupName() then local radioGroup = veafCombatZone.radioGroupsDict[zone:getRadioGroupName()] if not radioGroup then -- create the radio group menu radioGroup = veafRadio.addSubMenu(zone:getRadioGroupName(), veafCombatZone.combatZoneRootPath) veaf.loggers.get(veafCombatZone.Id):debug("created radio group %s", zone:getRadioGroupName()) veafCombatZone.radioGroupsDict[zone:getRadioGroupName()] = radioGroup end zone:setRadioParentPath(radioGroup) else zone:setRadioParentPath(veafCombatZone.combatZoneRootPath) end zone:updateRadioMenu(true) end end veafRadio.refreshRadioMenu() end function veafCombatZone.help(unitName) local text = 'Combat zones are defined by the mission maker\n' .. 'You can activate and desactivate them at will,\n' .. 'as well as ask for information, JTAC laser and smoke. \n\n' .. 'Combat operations are defined by the mission maker\n' .. 'A combat operation is a series of combat zones to complete,\n' .. 'You can ask information to get briefing and intel for current tasking orders.' veaf.outTextForGroup(unitName, text, 30) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafCombatZone.initialize() veaf.loggers.get(veafCombatZone.Id):info("Initializing module") veafCombatZone.buildRadioMenu() end veaf.loggers.get(veafCombatZone.Id):info(string.format("Loading version %s", veafCombatZone.Version)) ------------------ END script veafCombatZone.lua ------------------ ------------------ START script veafGrass.lua ------------------ ------------------------------------------------------------------ -- VEAF grass functions for DCS World -- By mitch (2018) -- -- Features: -- --------- -- * Script to build units on FARPS and grass runways -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafGrass = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafGrass.Id = "GRASS" --- Version. veafGrass.Version = "2.9.0" -- trace level, specific to this module --veafGrass.LogLevel = "trace" veaf.loggers.new(veafGrass.Id, veafGrass.LogLevel) veafGrass.DelayForStartup = 2 veafGrass.RadiusAroundFarp = 2000 -- these units will be placed in spawned FARPs warehouses and available for the dynamic slot mechanism veafGrass.helicoptersOnFARPs = { "SA342Mistral", "SA342Minigun", "SA342L", "SA342M", "UH-1H", "Mi-8MTV2", "Mi-8MT", "Mi-24P", "Mi-24V", "Bell-47", "UH-60L", "UH-60L_DAP", "AH-64D_BLK_II", "Bronco-OV-10A", "MH-60R", "OH-6A", "OH58D", "CH-47Fbl1" } ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------ -- veafGrass.buildGrassRunway -- Build a grass runway from grassRunwayUnit -- @param grassRunwayUnit a static unit object (right side) -- @return a named point if successful ------------------------------------------------------------------------------ function veafGrass.buildGrassRunway(grassRunwayUnit, hiddenOnMFD) veaf.loggers.get(veafGrass.Id):debug(string.format("veafGrass.buildGrassRunway()")) veaf.loggers.get(veafGrass.Id):trace(string.format("grassRunwayUnit=%s",veaf.p(grassRunwayUnit))) veaf.loggers.get(veafGrass.Id):trace(string.format("hiddenOnMFD=%s",veaf.p(hiddenOnMFD))) if not grassRunwayUnit then return nil end local name = grassRunwayUnit.unitName local runwayOrigin = grassRunwayUnit local tower = true local endMarkers = false -- runway length in meters local length = 600; -- a plot each XX meters local space = 50; -- runway width XX meters local width = 30; -- nb plots local nbPlots = math.ceil(length / space); local angle = math.floor(mist.utils.toDegree(runwayOrigin.heading)+0.5); -- create left origin from right origin local leftOrigin = { ["x"] = runwayOrigin.x + width * math.cos(mist.utils.toRadian(angle-90)), ["y"] = runwayOrigin.y + width * math.sin(mist.utils.toRadian(angle-90)), } local template = { ["category"] = runwayOrigin.category, ["categoryStatic"] = runwayOrigin.categoryStatic, ["coalition"] = runwayOrigin.coalition, ["country"] = runwayOrigin.country, ["countryId"] = runwayOrigin.countryId, ["heading"] = runwayOrigin.heading, ["shape_name"] = runwayOrigin.shape_name, ["type"] = runwayOrigin.type, ["hiddenOnMFD"] = hiddenOnMFD, } -- leftOrigin plot local leftOriginPlot = mist.utils.deepCopy(template) leftOriginPlot.x = leftOrigin.x leftOriginPlot.y = leftOrigin.y mist.dynAddStatic(leftOriginPlot) -- place plots for i = 1, nbPlots do -- right plot local leftPlot = mist.utils.deepCopy(template) leftPlot.x = runwayOrigin.x + i * space * math.cos(mist.utils.toRadian(angle)) leftPlot.y = runwayOrigin.y + i * space * math.sin(mist.utils.toRadian(angle)) mist.dynAddStatic(leftPlot) -- right plot local rightPlot = mist.utils.deepCopy(template) rightPlot.x = leftOrigin.x + i * space * math.cos(mist.utils.toRadian(angle)) rightPlot.y = leftOrigin.y + i * space * math.sin(mist.utils.toRadian(angle)) mist.dynAddStatic(rightPlot) end if (endMarkers) then -- close the runway with optional markers (airshow cones) template = { ["category"] = "Fortifications", ["categoryStatic"] = runwayOrigin.categoryStatic, ["coalition"] = runwayOrigin.coalition, ["country"] = runwayOrigin.country, ["countryId"] = runwayOrigin.countryId, ["heading"] = runwayOrigin.heading, ["shape_name"] = "Comp_cone", ["type"] = "Airshow_Cone", ["hiddenOnMFD"] = hiddenOnMFD, } -- right plot local leftPlot = mist.utils.deepCopy(template) leftPlot.x = runwayOrigin.x + (nbPlots+1) * space * math.cos(mist.utils.toRadian(angle)) leftPlot.y = runwayOrigin.y + (nbPlots+1) * space * math.sin(mist.utils.toRadian(angle)) mist.dynAddStatic(leftPlot) -- right plot local rightPlot = mist.utils.deepCopy(template) rightPlot.x = leftOrigin.x + (nbPlots+1) * space * math.cos(mist.utils.toRadian(angle)) rightPlot.y = leftOrigin.y + (nbPlots+1) * space * math.sin(mist.utils.toRadian(angle)) mist.dynAddStatic(rightPlot) end if (tower) then -- optionally add a tower at the start of the runway template = { ["category"] = "Fortifications", ["categoryStatic"] = runwayOrigin.categoryStatic, ["coalition"] = runwayOrigin.coalition, ["country"] = runwayOrigin.country, ["countryId"] = runwayOrigin.countryId, ["heading"] = runwayOrigin.heading, ["type"] = "house2arm", ["hiddenOnMFD"] = hiddenOnMFD, } -- tower local tower = mist.utils.deepCopy(template) tower.x = leftOrigin.x-60 + (nbPlots+1.2) * space * math.cos(mist.utils.toRadian(angle)) tower.y = leftOrigin.y-60 + (nbPlots+1.2) * space * math.sin(mist.utils.toRadian(angle)) mist.dynAddStatic(tower) end -- add the runway to the named points local point = { x = runwayOrigin.x+20 + (nbPlots+1) * space * math.cos(mist.utils.toRadian(angle)) + width/2 * math.cos(mist.utils.toRadian(angle-90)), y = math.floor(land.getHeight(leftOrigin) + 1), z = runwayOrigin.y+20 + (nbPlots+1) * space * math.sin(mist.utils.toRadian(angle)) + width/2 * math.cos(mist.utils.toRadian(angle-90)), atc = true, runways = { { hdg = (angle + 180) % 360, flare = "red"} } } return point end ------------------------------------------------------------------------------ -- veafGrass.buildFarpsUnits -- build FARP units on FARP with group name like "FARP " ------------------------------------------------------------------------------ function veafGrass.buildFarpsUnits(hiddenOnMFD) local farpUnits = {} local grassRunwayUnits = {} for name, unit in pairs(mist.DBs.unitsByName) do --veaf.loggers.get(veafGrass.Id):trace("buildFarpsUnits: testing " .. unit.type .. " " .. name) if name:upper():find('GRASS_RUNWAY') then grassRunwayUnits[name] = unit veaf.loggers.get(veafGrass.Id):trace(string.format("found grassRunwayUnits[%s]= %s", name, veaf.p(unit))) end --first two types should represent the same object depending on if you're on the MIST side or DCS side, as a safety added both if (unit.type == "SINGLE_HELIPAD" or unit.type == "FARP_SINGLE_01" or unit.type == "FARP" or unit.type == "Invisible FARP" or unit.type == "FARP_T") and name:upper():sub(1,5)=="FARP " then farpUnits[name] = unit veaf.loggers.get(veafGrass.Id):trace(string.format("found farpUnits[%s]= %s", name, veaf.p(unit))) end end veaf.loggers.get(veafGrass.Id):trace(string.format("farpUnits=%s",veaf.p(farpUnits))) veaf.loggers.get(veafGrass.Id):trace(string.format("grassRunwayUnits=%s",veaf.p(grassRunwayUnits))) for name, unit in pairs(farpUnits) do veaf.loggers.get(veafGrass.Id):trace(string.format("calling buildFarpsUnits(%s)",name)) veafGrass.buildFarpUnits(unit, grassRunwayUnits, nil, hiddenOnMFD) end end ---Browse all the FARP-type units and refill their warehouses function veafGrass.fillAllFarpWarehouses() veaf.loggers.get(veafGrass.Id):debug("veafGrass.fillAllFarpWarehouses()") local farpBases = {} local grassBases = {} local bases = world.getAirbases() for _, base in pairs(bases) do local name = base:getName() veaf.loggers.get(veafGrass.Id):trace("fillAllFarpWarehouse: testing %s", veaf.p(name)) local status, typeName = pcall(base.getTypeName, base) -- test cautiously if the base is a valid airbase, since DCS will either fail the getTypeName call or even crash when the airbase has been "moved" (e.g., by creating a new FARP with the same name) if status then if name:upper():find('GRASS_RUNWAY') then grassBases[name] = base veaf.loggers.get(veafGrass.Id):trace("found grassBase [%s]", veaf.p(name)) end --first two types should represent the same object depending on if you're on the MIST side or DCS side, as a safety added both if (typeName == "SINGLE_HELIPAD" or typeName == "FARP_SINGLE_01" or typeName == "FARP" or typeName == "Invisible FARP") then farpBases[name] = base veaf.loggers.get(veafGrass.Id):trace("found farpBase [%s]", veaf.p(name)) end else veaf.loggers.get(veafGrass.Id):warn("Airbase is not a valid object - getTypeName crashed - [%s]", veaf.p(name)) end end for _, base in pairs(grassBases) do veafGrass.fillFarpWarehouse(base) end for _, base in pairs(farpBases) do veafGrass.fillFarpWarehouse(base) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Very long table used to fill FARP warehouses ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafGrass.WAREHOUSE_ITEMS={[1]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=10},["initialAmount"]=100},[2]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=103},["initialAmount"]=100},[3]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1056},["initialAmount"]=100},[4]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=107},["initialAmount"]=100},[5]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=11},["initialAmount"]=100},[6]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=12},["initialAmount"]=100},[7]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=13},["initialAmount"]=100},[8]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=14},["initialAmount"]=100},[9]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1469},["initialAmount"]=100},[10]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1470},["initialAmount"]=100},[11]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=15},["initialAmount"]=100},[12]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=152},["initialAmount"]=100},[13]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1551},["initialAmount"]=100},[14]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1552},["initialAmount"]=100},[15]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1553},["initialAmount"]=100},[16]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1554},["initialAmount"]=100},[17]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1555},["initialAmount"]=100},[18]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1556},["initialAmount"]=100},[19]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1572},["initialAmount"]=100},[20]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1573},["initialAmount"]=100},[21]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=16},["initialAmount"]=100},[22]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1640},["initialAmount"]=100},[23]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1641},["initialAmount"]=100},[24]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1642},["initialAmount"]=100},[25]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=17},["initialAmount"]=100},[26]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1700},["initialAmount"]=100},[27]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1715},["initialAmount"]=100},[28]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=1716},["initialAmount"]=100},[29]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=2144},["initialAmount"]=100},[30]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=2145},["initialAmount"]=100},[31]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=2146},["initialAmount"]=100},[32]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=2380},["initialAmount"]=100},[33]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=2381},["initialAmount"]=100},[34]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=2382},["initialAmount"]=100},[35]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=2383},["initialAmount"]=100},[36]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=263},["initialAmount"]=100},[37]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=264},["initialAmount"]=100},[38]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=265},["initialAmount"]=100},[39]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=266},["initialAmount"]=100},[40]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=267},["initialAmount"]=100},[41]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=274},["initialAmount"]=100},[42]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=275},["initialAmount"]=100},[43]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=294},["initialAmount"]=100},[44]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=36},["initialAmount"]=100},[45]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=38},["initialAmount"]=100},[46]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=39},["initialAmount"]=100},[47]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=41},["initialAmount"]=100},[48]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=42},["initialAmount"]=100},[49]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=465},["initialAmount"]=100},[50]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=466},["initialAmount"]=100},[51]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=468},["initialAmount"]=100},[52]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=469},["initialAmount"]=100},[53]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=484},["initialAmount"]=100},[54]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=485},["initialAmount"]=100},[55]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=5},["initialAmount"]=100},[56]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=53},["initialAmount"]=100},[57]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=54},["initialAmount"]=100},[58]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=55},["initialAmount"]=100},[59]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=56},["initialAmount"]=100},[60]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=587},["initialAmount"]=100},[61]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=589},["initialAmount"]=100},[62]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=590},["initialAmount"]=100},[63]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=593},["initialAmount"]=100},[64]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=603},["initialAmount"]=100},[65]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=604},["initialAmount"]=100},[66]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=605},["initialAmount"]=100},[67]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=609},["initialAmount"]=100},[68]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=61},["initialAmount"]=100},[69]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=610},["initialAmount"]=100},[70]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=611},["initialAmount"]=100},[71]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=616},["initialAmount"]=100},[72]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=617},["initialAmount"]=100},[73]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=662},["initialAmount"]=100},[74]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=663},["initialAmount"]=100},[75]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=664},["initialAmount"]=100},[76]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=782},["initialAmount"]=100},[77]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=783},["initialAmount"]=100},[78]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=855},["initialAmount"]=100},[79]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=928},["initialAmount"]=100},[80]={["wsType"]={[1]=1,[2]=3,[3]=43,[4]=929},["initialAmount"]=100},[81]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=101},["initialAmount"]=5550},[82]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=1548},["initialAmount"]=5550},[83]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=1717},["initialAmount"]=5550},[84]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=1718},["initialAmount"]=5550},[85]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=1719},["initialAmount"]=5550},[86]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=1720},["initialAmount"]=5550},[87]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=1721},["initialAmount"]=5550},[88]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=19},["initialAmount"]=5550},[89]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2114},["initialAmount"]=1254},[90]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2138},["initialAmount"]=5550},[91]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2139},["initialAmount"]=5550},[92]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2140},["initialAmount"]=5550},[93]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2141},["initialAmount"]=5550},[94]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2142},["initialAmount"]=5550},[95]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2148},["initialAmount"]=5550},[96]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2149},["initialAmount"]=5550},[97]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2286},["initialAmount"]=5550},[98]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2287},["initialAmount"]=5550},[99]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2288},["initialAmount"]=5550},[100]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=2475},["initialAmount"]=5550},[101]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=26},["initialAmount"]=5550},[102]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=28},["initialAmount"]=5550},[103]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=424},["initialAmount"]=5550},[104]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=425},["initialAmount"]=5550},[105]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=426},["initialAmount"]=5550},[106]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=461},["initialAmount"]=5550},[107]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=463},["initialAmount"]=5550},[108]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=486},["initialAmount"]=5550},[109]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=59},["initialAmount"]=5550},[110]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=62},["initialAmount"]=5550},[111]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=63},["initialAmount"]=5550},[112]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=64},["initialAmount"]=5550},[113]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=65},["initialAmount"]=5550},[114]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=74},["initialAmount"]=5550},[115]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=78},["initialAmount"]=5550},[116]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=808},["initialAmount"]=5550},[117]={["wsType"]={[1]=4,[2]=15,[3]=44,[4]=95},["initialAmount"]=5550},[118]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=142},["initialAmount"]=5550},[119]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=173},["initialAmount"]=5550},[120]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=1762},["initialAmount"]=5550},[121]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=1763},["initialAmount"]=5550},[122]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=25},["initialAmount"]=5550},[123]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=29},["initialAmount"]=5550},[124]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=295},["initialAmount"]=5550},[125]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=296},["initialAmount"]=5550},[126]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=30},["initialAmount"]=5550},[127]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=301},["initialAmount"]=5550},[128]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=37},["initialAmount"]=5550},[129]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=462},["initialAmount"]=5550},[130]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=464},["initialAmount"]=5550},[131]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=665},["initialAmount"]=5550},[132]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=681},["initialAmount"]=5550},[133]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=94},["initialAmount"]=5550},[134]={["wsType"]={[1]=4,[2]=15,[3]=45,[4]=968},["initialAmount"]=5550},[135]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1057},["initialAmount"]=5550},[136]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1294},["initialAmount"]=5550},[137]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1295},["initialAmount"]=5550},[138]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=145},["initialAmount"]=5550},[139]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1544},["initialAmount"]=5550},[140]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1545},["initialAmount"]=5550},[141]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1546},["initialAmount"]=5550},[142]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1547},["initialAmount"]=5550},[143]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=160},["initialAmount"]=5550},[144]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=161},["initialAmount"]=5550},[145]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=170},["initialAmount"]=5550},[146]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=171},["initialAmount"]=5550},[147]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=174},["initialAmount"]=5550},[148]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=175},["initialAmount"]=5550},[149]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=176},["initialAmount"]=5550},[150]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1764},["initialAmount"]=5550},[151]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1765},["initialAmount"]=5550},[152]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1766},["initialAmount"]=5550},[153]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1767},["initialAmount"]=5550},[154]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1768},["initialAmount"]=5550},[155]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1769},["initialAmount"]=5550},[156]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=177},["initialAmount"]=5550},[157]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1770},["initialAmount"]=5550},[158]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1771},["initialAmount"]=5550},[159]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=18},["initialAmount"]=5550},[160]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1813},["initialAmount"]=5550},[161]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=183},["initialAmount"]=5550},[162]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=184},["initialAmount"]=5550},[163]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=1919},["initialAmount"]=5550},[164]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=20},["initialAmount"]=5550},[165]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2143},["initialAmount"]=5550},[166]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2476},["initialAmount"]=5550},[167]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2477},["initialAmount"]=5550},[168]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2478},["initialAmount"]=5550},[169]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2479},["initialAmount"]=5550},[170]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2480},["initialAmount"]=5550},[171]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2481},["initialAmount"]=5550},[172]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2482},["initialAmount"]=5550},[173]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2483},["initialAmount"]=5550},[174]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2484},["initialAmount"]=5550},[175]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2574},["initialAmount"]=5550},[176]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2575},["initialAmount"]=5550},[177]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2576},["initialAmount"]=5550},[178]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2577},["initialAmount"]=5550},[179]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=2578},["initialAmount"]=5550},[180]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=286},["initialAmount"]=5550},[181]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=300},["initialAmount"]=5550},[182]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=428},["initialAmount"]=5550},[183]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=429},["initialAmount"]=5550},[184]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=588},["initialAmount"]=5550},[185]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=596},["initialAmount"]=5550},[186]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=824},["initialAmount"]=5550},[187]={["wsType"]={[1]=4,[2]=15,[3]=46,[4]=825},["initialAmount"]=5550},[188]={["wsType"]={[1]=4,[2]=15,[3]=47,[4]=104},["initialAmount"]=5550},[189]={["wsType"]={[1]=4,[2]=15,[3]=47,[4]=108},["initialAmount"]=5550},[190]={["wsType"]={[1]=4,[2]=15,[3]=47,[4]=1100},["initialAmount"]=5550},[191]={["wsType"]={[1]=4,[2]=15,[3]=47,[4]=1549},["initialAmount"]=5550},[192]={["wsType"]={[1]=4,[2]=15,[3]=47,[4]=4},["initialAmount"]=5550},[193]={["wsType"]={[1]=4,[2]=15,[3]=47,[4]=679},["initialAmount"]=5550},[194]={["wsType"]={[1]=4,[2]=15,[3]=47,[4]=680},["initialAmount"]=5550},[195]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=1168},["initialAmount"]=5550},[196]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=1169},["initialAmount"]=5550},[197]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=1170},["initialAmount"]=5550},[198]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=1171},["initialAmount"]=5550},[199]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=1172},["initialAmount"]=5550},[200]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=1173},["initialAmount"]=5550},[201]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=1174},["initialAmount"]=5550},[202]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=297},["initialAmount"]=5550},[203]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=58},["initialAmount"]=5550},[204]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=608},["initialAmount"]=5550},[205]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=666},["initialAmount"]=5550},[206]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=765},["initialAmount"]=5550},[207]={["wsType"]={[1]=4,[2]=15,[3]=48,[4]=766},["initialAmount"]=5550},[208]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=1550},["initialAmount"]=5550},[209]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=172},["initialAmount"]=5550},[210]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=268},["initialAmount"]=5550},[211]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=269},["initialAmount"]=5550},[212]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=270},["initialAmount"]=5550},[213]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=271},["initialAmount"]=5550},[214]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=272},["initialAmount"]=5550},[215]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=273},["initialAmount"]=5550},[216]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=298},["initialAmount"]=5550},[217]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=427},["initialAmount"]=5550},[218]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=467},["initialAmount"]=5550},[219]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=470},["initialAmount"]=5550},[220]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=66},["initialAmount"]=5550},[221]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=667},["initialAmount"]=5550},[222]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=668},["initialAmount"]=5550},[223]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=67},["initialAmount"]=5550},[224]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=82},["initialAmount"]=5550},[225]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=83},["initialAmount"]=5550},[226]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=84},["initialAmount"]=5550},[227]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=85},["initialAmount"]=5550},[228]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=86},["initialAmount"]=5550},[229]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=87},["initialAmount"]=5550},[230]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=88},["initialAmount"]=5550},[231]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=89},["initialAmount"]=5550},[232]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=90},["initialAmount"]=5550},[233]={["wsType"]={[1]=4,[2]=15,[3]=50,[4]=91},["initialAmount"]=5550},[234]={["wsType"]={[1]=4,[2]=4,[3]=100,[4]=143},["initialAmount"]=100},[235]={["wsType"]={[1]=4,[2]=4,[3]=101,[4]=140},["initialAmount"]=100},[236]={["wsType"]={[1]=4,[2]=4,[3]=101,[4]=141},["initialAmount"]=100},[237]={["wsType"]={[1]=4,[2]=4,[3]=101,[4]=142},["initialAmount"]=100},[238]={["wsType"]={[1]=4,[2]=4,[3]=101,[4]=154},["initialAmount"]=100},[239]={["wsType"]={[1]=4,[2]=4,[3]=32,[4]=719},["initialAmount"]=100},[240]={["wsType"]={[1]=4,[2]=4,[3]=32,[4]=849},["initialAmount"]=100},[241]={["wsType"]={[1]=4,[2]=4,[3]=34,[4]=291},["initialAmount"]=100},[242]={["wsType"]={[1]=4,[2]=4,[3]=34,[4]=91},["initialAmount"]=100},[243]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=1},["initialAmount"]=100},[244]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=10},["initialAmount"]=100},[245]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=106},["initialAmount"]=100},[246]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=11},["initialAmount"]=100},[247]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=11037},["initialAmount"]=100},[248]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=11038},["initialAmount"]=100},[249]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=11039},["initialAmount"]=100},[250]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=13},["initialAmount"]=100},[251]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=135},["initialAmount"]=100},[252]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=136},["initialAmount"]=100},[253]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=14},["initialAmount"]=100},[254]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=15},["initialAmount"]=100},[255]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=16},["initialAmount"]=100},[256]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=18},["initialAmount"]=100},[257]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=19},["initialAmount"]=100},[258]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=2},["initialAmount"]=100},[259]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=21},["initialAmount"]=100},[260]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=22},["initialAmount"]=100},[261]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=23},["initialAmount"]=100},[262]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=24},["initialAmount"]=100},[263]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=26},["initialAmount"]=100},[264]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=265},["initialAmount"]=100},[265]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=266},["initialAmount"]=100},[266]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=267},["initialAmount"]=100},[267]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=268},["initialAmount"]=100},[268]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=269},["initialAmount"]=100},[269]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=27},["initialAmount"]=100},[270]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=270},["initialAmount"]=100},[271]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=3},["initialAmount"]=100},[272]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=306},["initialAmount"]=100},[273]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=307},["initialAmount"]=100},[274]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=308},["initialAmount"]=100},[275]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=309},["initialAmount"]=100},[276]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=310},["initialAmount"]=100},[277]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=320},["initialAmount"]=100},[278]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=321},["initialAmount"]=100},[279]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=322},["initialAmount"]=100},[280]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=327},["initialAmount"]=100},[281]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=333},["initialAmount"]=100},[282]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=334},["initialAmount"]=100},[283]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=335},["initialAmount"]=100},[284]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=336},["initialAmount"]=100},[285]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=337},["initialAmount"]=100},[286]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=338},["initialAmount"]=100},[287]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=339},["initialAmount"]=100},[288]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=368},["initialAmount"]=100},[289]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=371},["initialAmount"]=100},[290]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=372},["initialAmount"]=100},[291]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=395},["initialAmount"]=100},[292]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=396},["initialAmount"]=100},[293]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=397},["initialAmount"]=100},[294]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=4},["initialAmount"]=100},[295]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=403},["initialAmount"]=100},[296]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=405},["initialAmount"]=100},[297]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=409},["initialAmount"]=100},[298]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=410},["initialAmount"]=100},[299]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=412},["initialAmount"]=100},[300]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=425},["initialAmount"]=100},[301]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=426},["initialAmount"]=100},[302]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=429},["initialAmount"]=100},[303]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=446},["initialAmount"]=100},[304]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=7},["initialAmount"]=100},[305]={["wsType"]={[1]=4,[2]=4,[3]=7,[4]=9},["initialAmount"]=100},[306]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=11031},["initialAmount"]=100},[307]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=11035},["initialAmount"]=100},[308]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=11040},["initialAmount"]=100},[309]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=11050},["initialAmount"]=100},[310]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=11051},["initialAmount"]=100},[311]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=11052},["initialAmount"]=100},[312]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=11053},["initialAmount"]=100},[313]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=11054},["initialAmount"]=100},[314]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=11092},["initialAmount"]=100},[315]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=11093},["initialAmount"]=100},[316]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=130},["initialAmount"]=100},[317]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=132},["initialAmount"]=100},[318]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=133},["initialAmount"]=100},[319]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=138},["initialAmount"]=100},[320]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=139},["initialAmount"]=100},[321]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=263},["initialAmount"]=100},[322]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=264},["initialAmount"]=100},[323]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=271},["initialAmount"]=100},[324]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=272},["initialAmount"]=100},[325]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=273},["initialAmount"]=100},[326]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=274},["initialAmount"]=100},[327]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=278},["initialAmount"]=100},[328]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=279},["initialAmount"]=100},[329]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=280},["initialAmount"]=100},[330]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=281},["initialAmount"]=100},[331]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=282},["initialAmount"]=100},[332]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=283},["initialAmount"]=100},[333]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=284},["initialAmount"]=100},[334]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=287},["initialAmount"]=100},[335]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=289},["initialAmount"]=100},[336]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=290},["initialAmount"]=100},[337]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=292},["initialAmount"]=100},[338]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=293},["initialAmount"]=100},[339]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=295},["initialAmount"]=100},[340]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=296},["initialAmount"]=100},[341]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=297},["initialAmount"]=100},[342]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=298},["initialAmount"]=100},[343]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=301},["initialAmount"]=100},[344]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=303},["initialAmount"]=100},[345]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=304},["initialAmount"]=100},[346]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=305},["initialAmount"]=100},[347]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=311},["initialAmount"]=100},[348]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=332},["initialAmount"]=100},[349]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=352},["initialAmount"]=100},[350]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=353},["initialAmount"]=100},[351]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=354},["initialAmount"]=100},[352]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=355},["initialAmount"]=100},[353]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=362},["initialAmount"]=100},[354]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=363},["initialAmount"]=100},[355]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=373},["initialAmount"]=100},[356]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=39},["initialAmount"]=100},[357]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=399},["initialAmount"]=100},[358]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=40},["initialAmount"]=100},[359]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=407},["initialAmount"]=100},[360]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=41},["initialAmount"]=100},[361]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=415},["initialAmount"]=100},[362]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=416},["initialAmount"]=100},[363]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=422},["initialAmount"]=100},[364]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=423},["initialAmount"]=100},[365]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=424},["initialAmount"]=100},[366]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=430},["initialAmount"]=100},[367]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=431},["initialAmount"]=100},[368]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=432},["initialAmount"]=100},[369]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=433},["initialAmount"]=100},[370]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=434},["initialAmount"]=100},[371]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=435},["initialAmount"]=100},[372]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=436},["initialAmount"]=100},[373]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=437},["initialAmount"]=100},[374]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=44},["initialAmount"]=100},[375]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=443},["initialAmount"]=100},[376]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=445},["initialAmount"]=100},[377]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=45},["initialAmount"]=100},[378]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=46},["initialAmount"]=100},[379]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=47},["initialAmount"]=100},[380]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=48},["initialAmount"]=100},[381]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=49},["initialAmount"]=100},[382]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=51},["initialAmount"]=100},[383]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=53},["initialAmount"]=100},[384]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=54},["initialAmount"]=100},[385]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=55},["initialAmount"]=100},[386]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=56},["initialAmount"]=100},[387]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=58},["initialAmount"]=100},[388]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=59},["initialAmount"]=100},[389]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=60},["initialAmount"]=100},[390]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=61},["initialAmount"]=100},[391]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=62},["initialAmount"]=100},[392]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=63},["initialAmount"]=100},[393]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=64},["initialAmount"]=100},[394]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=65},["initialAmount"]=100},[395]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=66},["initialAmount"]=100},[396]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=68},["initialAmount"]=100},[397]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=70},["initialAmount"]=100},[398]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=71},["initialAmount"]=100},[399]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=72},["initialAmount"]=100},[400]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=73},["initialAmount"]=100},[401]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=74},["initialAmount"]=100},[402]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=75},["initialAmount"]=100},[403]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=76},["initialAmount"]=100},[404]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=77},["initialAmount"]=100},[405]={["wsType"]={[1]=4,[2]=4,[3]=8,[4]=78},["initialAmount"]=100},[406]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=1000},["initialAmount"]=100},[407]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=1002},["initialAmount"]=100},[408]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=1003},["initialAmount"]=100},[409]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=1004},["initialAmount"]=100},[410]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=1005},["initialAmount"]=100},[411]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=1006},["initialAmount"]=100},[412]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=1007},["initialAmount"]=100},[413]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=1009},["initialAmount"]=100},[414]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=2558},["initialAmount"]=100},[415]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=2559},["initialAmount"]=100},[416]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=2560},["initialAmount"]=100},[417]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=2561},["initialAmount"]=100},[418]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=2562},["initialAmount"]=100},[419]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=2563},["initialAmount"]=100},[420]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=837},["initialAmount"]=100},[421]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=839},["initialAmount"]=100},[422]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=94},["initialAmount"]=100},[423]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=95},["initialAmount"]=100},[424]={["wsType"]={[1]=4,[2]=5,[3]=32,[4]=999},["initialAmount"]=100},[425]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=11},["initialAmount"]=100},[426]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=12},["initialAmount"]=100},[427]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=14},["initialAmount"]=100},[428]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=287},["initialAmount"]=100},[429]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=288},["initialAmount"]=100},[430]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=289},["initialAmount"]=100},[431]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=290},["initialAmount"]=100},[432]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=291},["initialAmount"]=100},[433]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=292},["initialAmount"]=100},[434]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=293},["initialAmount"]=100},[435]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=351},["initialAmount"]=100},[436]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=36},["initialAmount"]=100},[437]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=38},["initialAmount"]=100},[438]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=39},["initialAmount"]=100},[439]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=41},["initialAmount"]=100},[440]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=42},["initialAmount"]=100},[441]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=43},["initialAmount"]=100},[442]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=448},["initialAmount"]=100},[443]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=459},["initialAmount"]=100},[444]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=469},["initialAmount"]=100},[445]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=47},["initialAmount"]=100},[446]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=476},["initialAmount"]=100},[447]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=48},["initialAmount"]=100},[448]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=72},["initialAmount"]=100},[449]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=85},["initialAmount"]=100},[450]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=86},["initialAmount"]=100},[451]={["wsType"]={[1]=4,[2]=5,[3]=36,[4]=92},["initialAmount"]=100},[452]={["wsType"]={[1]=4,[2]=5,[3]=37,[4]=3},["initialAmount"]=100},[453]={["wsType"]={[1]=4,[2]=5,[3]=37,[4]=330},["initialAmount"]=100},[454]={["wsType"]={[1]=4,[2]=5,[3]=37,[4]=347},["initialAmount"]=100},[455]={["wsType"]={[1]=4,[2]=5,[3]=37,[4]=384},["initialAmount"]=100},[456]={["wsType"]={[1]=4,[2]=5,[3]=37,[4]=4},["initialAmount"]=100},[457]={["wsType"]={[1]=4,[2]=5,[3]=37,[4]=437},["initialAmount"]=100},[458]={["wsType"]={[1]=4,[2]=5,[3]=37,[4]=62},["initialAmount"]=100},[459]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=18},["initialAmount"]=100},[460]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=20},["initialAmount"]=100},[461]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=23},["initialAmount"]=100},[462]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=263},["initialAmount"]=100},[463]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=265},["initialAmount"]=100},[464]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=267},["initialAmount"]=100},[465]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=295},["initialAmount"]=100},[466]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=299},["initialAmount"]=100},[467]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=301},["initialAmount"]=100},[468]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=302},["initialAmount"]=100},[469]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=319},["initialAmount"]=100},[470]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=324},["initialAmount"]=100},[471]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=35},["initialAmount"]=100},[472]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=45},["initialAmount"]=100},[473]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=480},["initialAmount"]=100},[474]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=481},["initialAmount"]=100},[475]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=482},["initialAmount"]=100},[476]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=77},["initialAmount"]=100},[477]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=87},["initialAmount"]=100},[478]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=88},["initialAmount"]=100},[479]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=91},["initialAmount"]=100},[480]={["wsType"]={[1]=4,[2]=5,[3]=38,[4]=93},["initialAmount"]=100},[481]={["wsType"]={[1]=4,[2]=5,[3]=49,[4]=11086},["initialAmount"]=100},[482]={["wsType"]={[1]=4,[2]=5,[3]=49,[4]=11087},["initialAmount"]=100},[483]={["wsType"]={[1]=4,[2]=5,[3]=49,[4]=11088},["initialAmount"]=100},[484]={["wsType"]={[1]=4,[2]=5,[3]=49,[4]=11089},["initialAmount"]=100},[485]={["wsType"]={[1]=4,[2]=5,[3]=49,[4]=427},["initialAmount"]=100},[486]={["wsType"]={[1]=4,[2]=5,[3]=49,[4]=63},["initialAmount"]=100},[487]={["wsType"]={[1]=4,[2]=5,[3]=49,[4]=64},["initialAmount"]=100},[488]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=11033},["initialAmount"]=100},[489]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=11034},["initialAmount"]=100},[490]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=255},["initialAmount"]=100},[491]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=256},["initialAmount"]=100},[492]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=257},["initialAmount"]=100},[493]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=258},["initialAmount"]=100},[494]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=259},["initialAmount"]=100},[495]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=260},["initialAmount"]=100},[496]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=261},["initialAmount"]=100},[497]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=268},["initialAmount"]=100},[498]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=269},["initialAmount"]=100},[499]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=270},["initialAmount"]=100},[500]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=271},["initialAmount"]=100},[501]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=272},["initialAmount"]=100},[502]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=273},["initialAmount"]=100},[503]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=274},["initialAmount"]=100},[504]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=275},["initialAmount"]=100},[505]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=276},["initialAmount"]=100},[506]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=277},["initialAmount"]=100},[507]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=278},["initialAmount"]=100},[508]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=279},["initialAmount"]=100},[509]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=280},["initialAmount"]=100},[510]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=281},["initialAmount"]=100},[511]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=282},["initialAmount"]=100},[512]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=283},["initialAmount"]=100},[513]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=284},["initialAmount"]=100},[514]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=285},["initialAmount"]=100},[515]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=30},["initialAmount"]=100},[516]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=31},["initialAmount"]=100},[517]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=312},["initialAmount"]=100},[518]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=313},["initialAmount"]=100},[519]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=314},["initialAmount"]=100},[520]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=315},["initialAmount"]=100},[521]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=316},["initialAmount"]=100},[522]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=317},["initialAmount"]=100},[523]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=318},["initialAmount"]=100},[524]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=32},["initialAmount"]=100},[525]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=321},["initialAmount"]=100},[526]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=322},["initialAmount"]=100},[527]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=323},["initialAmount"]=100},[528]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=325},["initialAmount"]=100},[529]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=326},["initialAmount"]=100},[530]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=327},["initialAmount"]=100},[531]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=328},["initialAmount"]=100},[532]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=329},["initialAmount"]=100},[533]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=33},["initialAmount"]=100},[534]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=331},["initialAmount"]=100},[535]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=332},["initialAmount"]=100},[536]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=333},["initialAmount"]=100},[537]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=334},["initialAmount"]=100},[538]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=335},["initialAmount"]=100},[539]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=336},["initialAmount"]=100},[540]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=337},["initialAmount"]=100},[541]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=338},["initialAmount"]=100},[542]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=339},["initialAmount"]=100},[543]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=34},["initialAmount"]=100},[544]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=363},["initialAmount"]=100},[545]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=364},["initialAmount"]=100},[546]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=374},["initialAmount"]=100},[547]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=38},["initialAmount"]=100},[548]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=385},["initialAmount"]=100},[549]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=386},["initialAmount"]=100},[550]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=387},["initialAmount"]=100},[551]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=388},["initialAmount"]=100},[552]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=389},["initialAmount"]=100},[553]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=390},["initialAmount"]=100},[554]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=391},["initialAmount"]=100},[555]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=392},["initialAmount"]=100},[556]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=412},["initialAmount"]=100},[557]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=413},["initialAmount"]=100},[558]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=449},["initialAmount"]=100},[559]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=483},["initialAmount"]=100},[560]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=484},["initialAmount"]=100},[561]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=485},["initialAmount"]=100},[562]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=486},["initialAmount"]=100},[563]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=487},["initialAmount"]=100},[564]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=488},["initialAmount"]=100},[565]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=5},["initialAmount"]=100},[566]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=6},["initialAmount"]=100},[567]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=69},["initialAmount"]=100},[568]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=7},["initialAmount"]=100},[569]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=70},["initialAmount"]=100},[570]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=71},["initialAmount"]=100},[571]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=72},["initialAmount"]=100},[572]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=75},["initialAmount"]=100},[573]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=79},["initialAmount"]=100},[574]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=9},["initialAmount"]=100},[575]={["wsType"]={[1]=4,[2]=5,[3]=9,[4]=90},["initialAmount"]=100},[576]={["wsType"]={[1]=4,[2]=7,[3]=32,[4]=11048},["initialAmount"]=100},[577]={["wsType"]={[1]=4,[2]=7,[3]=32,[4]=11056},["initialAmount"]=100},[578]={["wsType"]={[1]=4,[2]=7,[3]=32,[4]=11090},["initialAmount"]=100},[579]={["wsType"]={[1]=4,[2]=7,[3]=32,[4]=619},["initialAmount"]=100},[580]={["wsType"]={[1]=4,[2]=7,[3]=32,[4]=659},["initialAmount"]=100},[581]={["wsType"]={[1]=4,[2]=7,[3]=32,[4]=661},["initialAmount"]=100},[582]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=11044},["initialAmount"]=100},[583]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=11049},["initialAmount"]=100},[584]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=11091},["initialAmount"]=100},[585]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=144},["initialAmount"]=100},[586]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=145},["initialAmount"]=100},[587]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=146},["initialAmount"]=100},[588]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=147},["initialAmount"]=100},[589]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=148},["initialAmount"]=100},[590]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=149},["initialAmount"]=100},[591]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=150},["initialAmount"]=100},[592]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=151},["initialAmount"]=100},[593]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=155},["initialAmount"]=100},[594]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=158},["initialAmount"]=100},[595]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=159},["initialAmount"]=100},[596]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=181},["initialAmount"]=100},[597]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=182},["initialAmount"]=100},[598]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=183},["initialAmount"]=100},[599]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=184},["initialAmount"]=100},[600]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=185},["initialAmount"]=100},[601]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=186},["initialAmount"]=100},[602]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=256},["initialAmount"]=100},[603]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=257},["initialAmount"]=100},[604]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=258},["initialAmount"]=100},[605]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=275},["initialAmount"]=100},[606]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=276},["initialAmount"]=100},[607]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=277},["initialAmount"]=100},[608]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=299},["initialAmount"]=100},[609]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=30},["initialAmount"]=100},[610]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=31},["initialAmount"]=100},[611]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=32},["initialAmount"]=100},[612]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=326},["initialAmount"]=100},[613]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=329},["initialAmount"]=100},[614]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=33},["initialAmount"]=100},[615]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=330},["initialAmount"]=100},[616]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=331},["initialAmount"]=100},[617]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=34},["initialAmount"]=100},[618]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=340},["initialAmount"]=100},[619]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=341},["initialAmount"]=100},[620]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=342},["initialAmount"]=100},[621]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=35},["initialAmount"]=100},[622]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=350},["initialAmount"]=100},[623]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=359},["initialAmount"]=100},[624]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=360},["initialAmount"]=100},[625]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=361},["initialAmount"]=100},[626]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=364},["initialAmount"]=100},[627]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=365},["initialAmount"]=100},[628]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=366},["initialAmount"]=100},[629]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=367},["initialAmount"]=100},[630]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=37},["initialAmount"]=100},[631]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=374},["initialAmount"]=100},[632]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=375},["initialAmount"]=100},[633]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=376},["initialAmount"]=100},[634]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=377},["initialAmount"]=100},[635]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=378},["initialAmount"]=100},[636]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=379},["initialAmount"]=100},[637]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=380},["initialAmount"]=100},[638]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=381},["initialAmount"]=100},[639]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=382},["initialAmount"]=100},[640]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=383},["initialAmount"]=100},[641]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=384},["initialAmount"]=100},[642]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=385},["initialAmount"]=100},[643]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=386},["initialAmount"]=100},[644]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=387},["initialAmount"]=100},[645]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=388},["initialAmount"]=100},[646]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=389},["initialAmount"]=100},[647]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=390},["initialAmount"]=100},[648]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=391},["initialAmount"]=100},[649]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=392},["initialAmount"]=100},[650]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=393},["initialAmount"]=100},[651]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=401},["initialAmount"]=100},[652]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=402},["initialAmount"]=100},[653]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=440},["initialAmount"]=100},[654]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=441},["initialAmount"]=100},[655]={["wsType"]={[1]=4,[2]=7,[3]=33,[4]=442},["initialAmount"]=100},[656]={["wsType"]={[1]=4,[2]=8,[3]=10,[4]=255},["initialAmount"]=100},[657]={["wsType"]={[1]=4,[2]=8,[3]=10,[4]=406},["initialAmount"]=100},[658]={["wsType"]={[1]=4,[2]=8,[3]=11,[4]=319},["initialAmount"]=100},[659]={["wsType"]={[1]=4,[2]=8,[3]=11,[4]=398},["initialAmount"]=100}} ---Add everything to the FARP warehouse; since 2.8.x, the FARPs spawn empty, hence making it impossible to rearm or refuel (even with all the necessary vehicles) ---@param farp any the FARP to be filled function veafGrass.fillFarpWarehouse(farp) veaf.loggers.get(veafGrass.Id):debug("veafGrass.fillFarpWarehouse()") veaf.loggers.get(veafGrass.Id):trace("farp=[%s]", veaf.p(farp)) local farpName = farp.name veaf.loggers.get(veafGrass.Id):trace("farpName=[%s]", veaf.p(farpName)) local result = farpName ~= nil if not result then result, farpName = pcall(Unit.getUnitName, farp) end veaf.loggers.get(veafGrass.Id):trace("farpName=[%s]", veaf.p(farpName)) if not result then result, farpName = pcall(Group.getGroupName, farp) end veaf.loggers.get(veafGrass.Id):trace("farpName=[%s]", veaf.p(farpName)) if not result then result, farpName = pcall(Object.getName, farp) end veaf.loggers.get(veafGrass.Id):trace("farpName=[%s]", veaf.p(farpName)) if farpName then local farpAirbase = Airbase.getByName(farpName) if farpAirbase then local farpWarehouse = farpAirbase:getWarehouse() if farpWarehouse then --veaf.loggers.get(veafGrass.Id):trace("inventory = %s", veaf.p(farpWarehouse:getInventory())) for _, datas in ipairs(veafGrass.WAREHOUSE_ITEMS) do local rnd = math.random(1, 100) farpWarehouse:setItem(datas.wsType, 5000+rnd) end for i = 0, 3, 1 do local rnd = math.random(1, 100) farpWarehouse:setLiquidAmount(i,50000+rnd) end for i = 0, 3 do veaf.loggers.get(veafGrass.Id):trace("getLiquidAmount(%s) = %s", i, veaf.p(farpWarehouse:getLiquidAmount(i))) end for _, aircraft_type in pairs(veafGrass.helicoptersOnFARPs) do farpWarehouse:addItem(aircraft_type, 999) end else veaf.loggers.get(veafGrass.Id):error("Airbase.getByName([%s]):getWarehouse() returned null", veaf.p(farpName)) end else veaf.loggers.get(veafGrass.Id):error("Airbase.getByName([%s]) returned null", veaf.p(farpName)) end veaf.loggers.get(veafGrass.Id):debug("FARP [%s] successfully replenished", veaf.p(farpName)) end end ------------------------------------------------------------------------------ -- build nice FARP units arround the FARP -- @param unit farp : the FARP unit ------------------------------------------------------------------------------ function veafGrass.buildFarpUnits(farp, grassRunwayUnits, groupName, hiddenOnMFD, noFarpMarkers, code, freq, mod) veaf.loggers.get(veafGrass.Id):debug("buildFarpUnits()") veaf.loggers.get(veafGrass.Id):trace("farp=%s",farp) veaf.loggers.get(veafGrass.Id):trace("grassRunwayUnits=%s",grassRunwayUnits) veaf.loggers.get(veafGrass.Id):trace("hiddenOnMFD=%s",hiddenOnMFD) veaf.loggers.get(veafGrass.Id):trace("noFarpMarkers=%s",noFarpMarkers) veaf.loggers.get(veafGrass.Id):trace("code=%s",code) veaf.loggers.get(veafGrass.Id):trace("freq=%s",freq) veaf.loggers.get(veafGrass.Id):trace("mod=%s",mod) local freq = freq or math.random(90)+100 local mod = mod or "X" local code = code or "FRP" -- add FARP to CTLD FOBs and logistic units local name = farp.name if not name then name = farp.unitName end if not name then name = farp.groupName end if ctld then table.insert(ctld.builtFOBS, name) table.insert(ctld.logisticUnits, name) end local farpUnitNameCounter=1 local farpCoalition = farp.coalition local farpCoalitionNumber = farp.coalition if type(farpCoalition == "number") then if farpCoalition == 1 then farpCoalition = "red" else farpCoalition = "blue" end end if type(farpCoalition == 'string') then if farpCoalition == "red" then farpCoalitionNumber = 1 else farpCoalitionNumber = 2 end end local farpHeading = farp.heading or 0 local angle = mist.utils.toDegree(farpHeading) local tentDistance = 100 local tentSpacing = 30 local otherDistance = 85 local otherSpacing = 15 local unitsDistance = 75 -- fix distances on FARPs if farp.type == "SINGLE_HELIPAD" or farp.type == "FARP_SINGLE_01" or farp.type == "FARP" or farp.type == "Invisible FARP" then tentDistance = 200 unitsDistance = 150 otherDistance = 130 end local tentOrigin = { ["x"] = farp.x + tentDistance * math.cos(mist.utils.toRadian(angle)), ["y"] = farp.y + tentDistance * math.sin(mist.utils.toRadian(angle)), } -- create tents for j = 1,2 do for i = 1,3 do local tent = { ["unitName"] = string.format("FARP %s unit #%d", farp.groupName, farpUnitNameCounter), ["category"] = 'static', ["categoryStatic"] = 'Fortifications', ["coalition"] = farpCoalition, ["country"] = farp.country, ["countryId"] = farp.countryId, ["heading"] = mist.utils.toRadian(angle-90), ["type"] = 'FARP Tent', ["x"] = tentOrigin.x + (i-1) * tentSpacing * math.cos(mist.utils.toRadian(angle)) - (j-1) * tentSpacing * math.sin(mist.utils.toRadian(angle)), ["y"] = tentOrigin.y + (i-1) * tentSpacing * math.sin(mist.utils.toRadian(angle)) + (j-1) * tentSpacing * math.cos(mist.utils.toRadian(angle)), ["hiddenOnMFD"] = hiddenOnMFD, } if groupName then tent["groupName"] = groupName end mist.dynAddStatic(tent) farpUnitNameCounter = farpUnitNameCounter + 1 end end -- add visible markers to the invisible farps if farp.type == "Invisible FARP" and not noFarpMarkers then local markerDistance = 25 local markerAngle = -45 local markerUnit1 = { ["unitName"] = string.format("FARP %s unit #%d", farp.groupName, farpUnitNameCounter), ["category"] = "Unarmed", ["type"] = "M978 HEMTT Tanker", ["coalition"] = farpCoalition, ["country"] = farp.country, ["countryId"] = farp.countryId, ["heading"] = mist.utils.toRadian(angle-90), ["x"] = farp.x - markerDistance * math.cos(mist.utils.toRadian(angle + markerAngle)), ["y"] = farp.y - markerDistance * math.sin(mist.utils.toRadian(angle + markerAngle)), ["hiddenOnMFD"] = hiddenOnMFD, } if groupName then markerUnit1["groupName"] = groupName end mist.dynAddStatic(markerUnit1) farpUnitNameCounter = farpUnitNameCounter + 1 local markerUnit2 = { ["unitName"] = string.format("FARP %s unit #%d", farp.groupName, farpUnitNameCounter), ["category"] = "Fortifications", ["shape_name"] = "H-Windsock_RW", ["type"] = "Windsock", ["coalition"] = farpCoalition, ["country"] = farp.country, ["countryId"] = farp.countryId, ["heading"] = mist.utils.toRadian(angle-90), ["x"] = farp.x - markerDistance * math.cos(mist.utils.toRadian(angle - markerAngle)), ["y"] = farp.y - markerDistance * math.sin(mist.utils.toRadian(angle - markerAngle)), ["hiddenOnMFD"] = hiddenOnMFD, } if groupName then markerUnit2["groupName"] = groupName end mist.dynAddStatic(markerUnit2) farpUnitNameCounter = farpUnitNameCounter + 1 end -- spawn other static units local otherUnits={ 'FARP Fuel Depot', 'FARP Ammo Dump Coating', 'GeneratorF', } local otherOrigin = { ["x"] = farp.x + otherDistance * math.cos(mist.utils.toRadian(angle)), ["y"] = farp.y + otherDistance * math.sin(mist.utils.toRadian(angle)), } for j,typeName in ipairs(otherUnits) do local otherUnit = { ["unitName"] = string.format("FARP %s unit #%d", farp.groupName, farpUnitNameCounter), ["category"] = 'static', ["categoryStatic"] = 'Fortifications', ["coalition"] = farpCoalition, ["country"] = farp.country, ["countryId"] = farp.countryId, ["heading"] = mist.utils.toRadian(angle-90), ["type"] = typeName, ["x"] = otherOrigin.x - (j-1) * otherSpacing * math.sin(mist.utils.toRadian(angle)), ["y"] = otherOrigin.y + (j-1) * otherSpacing * math.cos(mist.utils.toRadian(angle)), ["hiddenOnMFD"] = hiddenOnMFD, } if groupName then otherUnit["groupName"] = groupName end mist.dynAddStatic(otherUnit) farpUnitNameCounter = farpUnitNameCounter + 1 end -- create Windsock local windsockDistance = 50 local windsockAngle = 45 -- fix Windsock position on FARPs if farp.type == "SINGLE_HELIPAD" or farp.type == "FARP_SINGLE_01" or farp.type == "FARP" or farp.type == "Invisible FARP" then windsockDistance = 120 windsockAngle = 0 end local windsockUnit = { ["unitName"] = string.format("FARP %s unit #%d", farp.groupName, farpUnitNameCounter), ["category"] = 'static', ["categoryStatic"] = 'Fortifications', ["shape_name"] = "H-Windsock_RW", ["type"] = "Windsock", ["coalition"] = farpCoalition, ["country"] = farp.country, ["countryId"] = farp.countryId, ["heading"] = mist.utils.toRadian(angle-90), ["x"] = farp.x + windsockDistance * math.cos(mist.utils.toRadian(angle + windsockAngle)), ["y"] = farp.y + windsockDistance * math.sin(mist.utils.toRadian(angle + windsockAngle)), ["hiddenOnMFD"] = hiddenOnMFD, } if groupName then windsockUnit["groupName"] = groupName end mist.dynAddStatic(windsockUnit) farpUnitNameCounter = farpUnitNameCounter + 1 -- on FARP unit, place a second windsock, at 90° if farp.type == 'FARP' then local windsockUnit = { ["unitName"] = string.format("FARP %s unit #%d", farp.groupName, farpUnitNameCounter), ["category"] = 'static', ["categoryStatic"] = 'Fortifications', ["shape_name"] = "H-Windsock_RW", ["type"] = "Windsock", ["coalition"] = farpCoalition, ["country"] = farp.country, ["countryId"] = farp.countryId, ["heading"] = mist.utils.toRadian(angle-90), ["x"] = farp.x + windsockDistance * math.cos(mist.utils.toRadian(angle + windsockAngle - 90)), ["y"] = farp.y + windsockDistance * math.sin(mist.utils.toRadian(angle + windsockAngle - 90)), ["hiddenOnMFD"] = hiddenOnMFD, } if groupName then windsockUnit["groupName"] = groupName end mist.dynAddStatic(windsockUnit) farpUnitNameCounter = farpUnitNameCounter + 1 end -- spawn a FARP escort group local farpEscortUnitsNames={ blue = { "Hummer", "M978 HEMTT Tanker", "M 818", "M 818", "Hummer", }, red = { "ATZ-10", "ATZ-10", "Ural-4320 APA-5D", "Ural-375", "Ural-375", "Ural-375 PBU", } } local unitsSpacing=6 local unitsOrigin = { x = farp.x + unitsDistance * math.cos(mist.utils.toRadian(angle)), y = farp.y + unitsDistance * math.sin(mist.utils.toRadian(angle)), } local farpEscortGroup = { ["category"] = 'vehicle', ["coalition"] = farpCoalition, ["country"] = farp.country, ["countryId"] = farp.countryId, ["groupName"] = farp.groupName, ["units"] = {}, ["hiddenOnMFD"] = hiddenOnMFD, } if groupName then farpEscortGroup["groupName"] = groupName end for j,typeName in ipairs(farpEscortUnitsNames[farpCoalition]) do local escortUnit = { ["unitName"] = string.format("FARP %s unit #%d", farp.groupName, farpUnitNameCounter), ["heading"] = mist.utils.toRadian(angle-135), -- parked \\\\\ ["type"] = typeName, ["x"] = unitsOrigin.x - (j-1) * unitsSpacing * math.sin(mist.utils.toRadian(angle)), ["y"] = unitsOrigin.y + (j-1) * unitsSpacing * math.cos(mist.utils.toRadian(angle)), ["skill"] = "Random", } table.insert(farpEscortGroup.units, escortUnit) farpUnitNameCounter = farpUnitNameCounter + 1 end mist.dynAdd(farpEscortGroup) -- add the FARP to the named points local farpNamedPoint = { x = farp.x, y = math.floor(land.getHeight(farp) + 1), z = farp.y, atc = true, runways = {} } -- add the FARP to the named points local beaconPoint = { x = farp.x + 250, y = math.floor(land.getHeight(farp) + 1), z = farp.y } farpNamedPoint.tower = "No Control" if ctld then -- spawn tacan mod = string.upper(mod) local tacanGroupName = string.format("TACAN %s - %s%s", tostring(code), tostring(freq), tostring(mod)) veaf.loggers.get(veafGrass.Id):trace(string.format("tacanGroupName=%s", tostring(tacanGroupName))) veaf.loggers.get(veafGrass.Id):trace(string.format("freq=%s", tostring(freq))) veaf.loggers.get(veafGrass.Id):trace(string.format("mod=%s", tostring(mod))) local txFreq = (1025 + freq - 1) * 1000000 local rxFreq = (962 + freq - 1) * 1000000 if (freq < 64 and mod == "Y") or (freq >= 64 and mod == "X") then rxFreq = (1088 + freq - 1) * 1000000 end veaf.loggers.get(veafGrass.Id):trace(string.format("txFreq=%s", tostring(txFreq))) veaf.loggers.get(veafGrass.Id):trace(string.format("rxFreq=%s", tostring(rxFreq))) local command = { id = 'ActivateBeacon', params = { type = 4, system = 18, callsign = code, frequency = rxFreq, AA = false, channel = freq, bearing = true, modeChannel = mod, } } veaf.loggers.get(veafGrass.Id):trace(string.format("setting %s", veaf.p(command))) local spawnedGroup = ctld.spawnRadioBeaconUnit(beaconPoint, farp.country, tacanGroupName, tacanGroupName) local controller = spawnedGroup:getController() controller:setCommand(command) veaf.loggers.get(veafGrass.Id):trace(string.format("done setting TACAN command")) -- spawn CTLD beacon local _beaconInfo = ctld.createRadioBeacon(beaconPoint, farpCoalitionNumber, farp.country, farp.unitName or farp.name, -1, true) if _beaconInfo ~= nil then farpNamedPoint.tacan = string.format("ADF : %.2f KHz - %.2f MHz - %.2f MHz FM - %s", _beaconInfo.vhf / 1000, _beaconInfo.uhf / 1000000, _beaconInfo.fm / 1000000, tacanGroupName) veaf.loggers.get(veafGrass.Id):trace(string.format("farpNamedPoint.tacan=%s", veaf.p(farpNamedPoint.tacan))) end end -- search for an associated grass runway if (grassRunwayUnits) then local grassRunwayUnit = nil for name, unitDef in pairs(grassRunwayUnits) do local unit = Unit.getByName(name) if not unit then unit = StaticObject.getByName(name) end if unit then local pos = unit:getPosition().p if pos then -- you never know O.o local distanceFromCenter = ((pos.x - farp.x)^2 + (pos.z - farp.y)^2)^0.5 veaf.loggers.get(veafGrass.Id):trace(string.format("name=%s; distanceFromCenter=%s", tostring(name), veaf.p(distanceFromCenter))) if distanceFromCenter <= veafGrass.RadiusAroundFarp then grassRunwayUnit = unitDef break end end end end if grassRunwayUnit then veaf.loggers.get(veafGrass.Id):trace(string.format("found grassRunwayUnit %s", veaf.p(grassRunwayUnit))) local grassNamedPoint = veafGrass.buildGrassRunway(grassRunwayUnit, hiddenOnMFD) if grassNamedPoint then farpNamedPoint.x = grassNamedPoint.x farpNamedPoint.y = grassNamedPoint.y farpNamedPoint.z = grassNamedPoint.z farpNamedPoint.atc = grassNamedPoint.atc farpNamedPoint.runways = grassNamedPoint.runways end end end veaf.loggers.get(veafGrass.Id):trace(string.format("farpNamedPoint=%s", veaf.p(farpNamedPoint))) veafNamedPoints.addPoint(farp.unitName or farp.name, farpNamedPoint) veaf.loggers.get(veafGrass.Id):trace(string.format("calling fillFarpWarehouse(%s)",name)) veafGrass.fillFarpWarehouse(farp) end --- --- called from veafEventHandler when a unit is created function veafGrass.onBirth(event) --veaf.loggers.get(veafGrass.Id):trace(string.format("onBirth(%s)",veaf.p(event))) -- find the originator unit local unitName = nil if event.initiator ~= nil then unitName = event.initiator.unitName end if not unitName then veaf.loggers.get(veafGrass.Id):warn("no unitname found in event %s", veaf.p(event)) return end if mist.DBs.humansByName[unitName] then -- it's a human unit veaf.loggers.get(veafGrass.Id):debug("caught event BIRTH for human unit [%s]", veaf.p(unitName)) local _unit = event.initiator if _unit ~= nil then -- refill all farp warehouses, to work around a DCS bug where the warehouses are spawned empty and their content is not synced over the network veafGrass.fillAllFarpWarehouses() end end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafGrass.initialize() -- delay all these functions 30 seconds (to ensure that the other modules are loaded) -- auto generate FARP units (hide these units on MFDs as they create clutter for nothing since the FARP already shows or not depending on what the Mission maker wanted, regardless, don't show them) mist.scheduleFunction(veafGrass.buildFarpsUnits,{true},timer.getTime()+veafGrass.DelayForStartup) veafEventHandler.addCallback("veafGrass.OnBirth", {"S_EVENT_BIRTH", "S_EVENT_PLAYER_ENTER_UNIT"}, veafGrass.onBirth) end veaf.loggers.get(veafGrass.Id):info(string.format("Loading version %s", veafGrass.Version)) ------------------ END script veafGrass.lua ------------------ ------------------ START script veafHoundElintHelper.lua ------------------ ------------------------------------------------------------------ -- VEAF helper for Hound-Elint -- By zip (2021) -- -- Features: -- --------- -- * This module offers support for integrating Hound-Elint in a mission -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafHoundElint = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafHoundElint.Id = "HOUND" --- Version. veafHoundElint.Version = "1.1.1" -- trace level, specific to this module --veafHoundElint.LogLevel = "debug" veaf.loggers.new(veafHoundElint.Id, veafHoundElint.LogLevel) -- delay before the mission groups are added to the Hound system at start veafHoundElint.DelayForStartup = 1 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafHoundElint.initialized = false veafHoundElint.prefix = nil veafHoundElint.redParameters = {} veafHoundElint.blueParameters = {} veafHoundElint.redHound = nil veafHoundElint.blueHound = nil veafHoundElint.elintUnitsTypes = {} veafHoundElint.globalSectorName = "GLOBAL" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- core functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafHoundElint.getHoundOfCoalition(coa) --veaf.loggers.get(veafHoundElint.Id):trace(string.format("getHoundOfCoalition(%s)", veaf.p(coa))) local hound = nil if coa == coalition.side.RED then hound = veafHoundElint.redHound elseif coa == coalition.side.BLUE then hound = veafHoundElint.blueHound end return hound end function veafHoundElint.addPlatformToSystem(dcsGroup, alreadyAddedUnits, atMissionStart) if not veafHoundElint.initialized then return false end if not dcsGroup then veaf.loggers.get(veafHoundElint.Id):error("group does not exist") return false end local groupName = dcsGroup:getName() local coa = dcsGroup:getCoalition() local hound = veafHoundElint.getHoundOfCoalition(coa) veaf.loggers.get(veafHoundElint.Id):debug(string.format("addPlatformToSystem(%s) to %s", veaf.p(groupName), veaf.p(veaf.ifnn(hound,"name")))) veaf.loggers.get(veafHoundElint.Id):trace(string.format("atMissionStart=%s", veaf.p(atMissionStart))) local batchMode = (alreadyAddedUnits ~= nil) local alreadyAddedUnits = alreadyAddedUnits or {} if not(hound) then veaf.loggers.get(veafHoundElint.Id):error(string.format("no Hound system for the coalition of %s", veaf.p(groupName))) return false end local didSomething = false local _addUnitToSystem = function(dcsUnit, isFunctional) if not(atMissionStart) or isFunctional then local unitName = dcsUnit:getName() local unitType = dcsUnit:getDesc()["typeName"] veaf.loggers.get(veafHoundElint.Id):trace(string.format("checking unit %s of type %s", veaf.p(unitName), veaf.p(unitType))) -- check if the unitType is supported by Hound Elint if veafHoundElint.elintUnitsTypes[unitType] then veaf.loggers.get(veafHoundElint.Id):trace(string.format("-> supported elint type")) -- check the unit name vs the prefix if veafHoundElint.prefix then local _p1, _p2 = unitName:lower():find(veafHoundElint.prefix:lower()) veaf.loggers.get(veafHoundElint.Id):trace(string.format("_p1=%s", veaf.p(_p1))) veaf.loggers.get(veafHoundElint.Id):trace(string.format("_p2=%s", veaf.p(_p2))) if _p2 and _p1 == 1 then -- found the prefix at the beginning of the name if not(alreadyAddedUnits[unitName]) then veaf.loggers.get(veafHoundElint.Id):trace(string.format("adding a platform : %s", unitName)) local added = hound:addPlatform(unitName) -- no actual return value -- todo check if ok when HoundElint will give us a return value if added then didSomething = true alreadyAddedUnits[unitName] = true veaf.loggers.get(veafHoundElint.Id):trace(string.format("adding a platform -> OK")) end end end end end end end --veaf.loggers.get(veafHoundElint.Id):trace(string.format("batchMode = %s", veaf.p(batchMode))) veaf.loggers.get(veafHoundElint.Id):trace(string.format("dcsGroup=%s", veaf.p(mist.utils.deepCopy(dcsGroup)))) if Group.getByName(groupName) then for _, dcsUnit in pairs(dcsGroup:getUnits()) do veaf.loggers.get(veafHoundElint.Id):trace(string.format("dcsUnit.getName=%s", veaf.p(veaf.ifnn(dcsUnit, "getName")))) veaf.loggers.get(veafHoundElint.Id):trace(string.format("dcsUnit:isActive()=%s", veaf.p(dcsUnit:isActive()))) _addUnitToSystem(dcsUnit, dcsUnit:isActive()) end elseif StaticObject.getByName(groupName) then veaf.loggers.get(veafHoundElint.Id):trace("Group is Static") veaf.loggers.get(veafHoundElint.Id):trace(string.format("dcsGroup:isExist()=%s", veaf.p(dcsGroup:isExist()))) _addUnitToSystem(dcsGroup, dcsGroup:isExist()) end if didSomething and not(batchMode) then veaf.loggers.get(veafHoundElint.Id):trace(string.format("reactivating the Elint system")) -- reactivate the system hound:systemOn() end return didSomething end local function initializeHoundSystem(coa, parameters, atMissionStart) local hound = veafHoundElint.getHoundOfCoalition(coa) veaf.loggers.get(veafHoundElint.Id):debug(string.format("initializeHoundSystem %s",tostring(hound.name))) veaf.loggers.get(veafHoundElint.Id):debug(string.format("atMissionStart=%s",veaf.p(atMissionStart))) local alreadyAddedUnits = {} local dcsGroups = coalition.getGroups(coa) for _, dcsGroup in pairs(dcsGroups) do veafHoundElint.addPlatformToSystem(dcsGroup, alreadyAddedUnits, atMissionStart) end if parameters then if parameters.markers then hound:enableMarkers(HOUND.MARKER.DIAMOND) veaf.loggers.get(veafHoundElint.Id):debug("enabled markers") end if parameters.platformPositionErrors then hound:enablePlatformPosErrors() veaf.loggers.get(veafHoundElint.Id):debug("enabled platformPositionErrors") end if parameters.disableBDA then hound:disableBDA() veaf.loggers.get(veafHoundElint.Id):debug("disabled BDA") end if parameters.NATO_SectorCallsigns then hound:useNATOCallsignes(true) veaf.loggers.get(veafHoundElint.Id):debug("using NATO callsigns for zones") end if parameters.NATOmessages then hound:enableNATO() veaf.loggers.get(veafHoundElint.Id):debug("using NATO message format") end if parameters.ATISinterval and type(parameters.ATISinterval) == 'number' and parameters.ATISinterval > 0 then hound:setAtisUpdateInterval(parameters.ATISinterval) veaf.loggers.get(veafHoundElint.Id):debug(string.format("ATIS interval set to %s", veaf.p(parameters.ATISinterval))) end if parameters.preBriefedContacts then for _,name in pairs(parameters.preBriefedContacts) do veaf.loggers.get(veafHoundElint.Id):debug(string.format("Attempting to add Pre-brief target named %s", veaf.p(name))) if name and type(name) == 'string' and (Group.getByName(name) or Unit.getByName(name)) then hound:preBriefedContact(name) veaf.loggers.get(veafHoundElint.Id):debug("Pre-brief target added") end end end if parameters.debug then hound:onScreenDebug(true) veaf.loggers.get(veafHoundElint.Id):debug("Debug enabled") end end if parameters.sectors and type(parameters.sectors) == 'table' then veaf.loggers.get(veafHoundElint.Id):debug("Checking sectors...") for SectorName, sectorParameters in pairs(parameters.sectors) do local skip = false veaf.loggers.get(veafHoundElint.Id):debug(string.format("Given Sector Name : %s", veaf.p(SectorName))) veaf.loggers.get(veafHoundElint.Id):trace(string.format("Sector Params : %s", veaf.p(sectorParameters))) local SectorName = SectorName if not SectorName then veaf.loggers.get(veafHoundElint.Id):debug("No SectorName has been given...") SectorName = "default" skip = true end if SectorName ~= "default" and tostring(SectorName) then SectorName = tostring(SectorName) veaf.loggers.get(veafHoundElint.Id):debug(string.format("Retained Sector Name : %s", veaf.p(SectorName))) local sector = hound:addSector(SectorName) if sector then veaf.loggers.get(veafHoundElint.Id):debug("Added sector !") if SectorName:lower() ~= veafHoundElint.globalSectorName:lower() then veaf.loggers.get(veafHoundElint.Id):debug("Setting zone for sector...") hound:setZone(SectorName, SectorName) if not hound:getZone(SectorName) then veaf.loggers.get(veafHoundElint.Id):debug("Could not find zone for sector...") hound:removeSector(SectorName) skip = true else veaf.loggers.get(veafHoundElint.Id):debug("Zone found and set") end else veaf.loggers.get(veafHoundElint.Id):debug("No zone needs to be set for global sector") end else veaf.loggers.get(veafHoundElint.Id):debug("No sectors added, Hound problem...") skip = true end elseif SectorName ~= "default" then veaf.loggers.get(veafHoundElint.Id):debug("Given Sector Name could not be converted to string...") skip = true end if not skip then if veafHoundElint.hasSectorCallsign(sectorParameters) then veaf.loggers.get(veafHoundElint.Id):debug("Setting sector callsign...") if sectorParameters.callsign == true or not tostring(sectorParameters.callsign) then veaf.loggers.get(veafHoundElint.Id):debug("Using sector name as callsign") sectorParameters.callsign = SectorName end veaf.loggers.get(veafHoundElint.Id):trace(string.format("Callsign : %s", veaf.p(tostring(sectorParameters.callsign)))) hound:setCallsign(SectorName, tostring(sectorParameters.callsign)) end if veafHoundElint.hasTransmitterUnit(sectorParameters) then veaf.loggers.get(veafHoundElint.Id):debug("Setting transmitter unit...") veaf.loggers.get(veafHoundElint.Id):trace(string.format("transmitter unitName : %s", veaf.p(tostring(sectorParameters.transmitterUnit)))) hound:setTransmitter(SectorName, tostring(sectorParameters.transmitterUnit)) end if veafHoundElint.hasAtis(sectorParameters) then veaf.loggers.get(veafHoundElint.Id):debug("Setting up ATIS...") veaf.loggers.get(veafHoundElint.Id):trace(string.format("ATIS params : %s", veaf.p(sectorParameters.atis))) hound:enableAtis(SectorName, sectorParameters.atis) if sectorParameters.atis.reportEWR then veaf.loggers.get(veafHoundElint.Id):debug("ATIS will report EWRs as threats") hound:reportEWR(SectorName, sectorParameters.atis.reportEWR) end end if veafHoundElint.hasController(sectorParameters) then veaf.loggers.get(veafHoundElint.Id):debug("Setting up Controller...") veaf.loggers.get(veafHoundElint.Id):trace(string.format("Controller params : %s", veaf.p(sectorParameters.controller))) hound:enableController(SectorName, sectorParameters.controller) local textMode = not(veafHoundElint.hasControllerVoice(sectorParameters)) if textMode then veaf.loggers.get(veafHoundElint.Id):debug("Controller is text only") hound:enableText(SectorName) end end if veafHoundElint.hasNotifier(sectorParameters) then veaf.loggers.get(veafHoundElint.Id):debug("Setting up Notifier...") veaf.loggers.get(veafHoundElint.Id):trace(string.format("Notifier params : %s", veaf.p(sectorParameters.notifier))) hound:enableNotifier(SectorName, sectorParameters.notifier) end if veafHoundElint.hasNoAlerts(sectorParameters) then veaf.loggers.get(veafHoundElint.Id):debug("Disabling alerts for controller/ATIS") hound:disableAlerts(SectorName) end if veafHoundElint.hasNoTTS(sectorParameters) then veaf.loggers.get(veafHoundElint.Id):debug("Disabling TTS overall") hound:disableTTS(SectorName) end else veaf.loggers.get(veafHoundElint.Id):debug("Skipping sector") end end else veaf.loggers.get(veafHoundElint.Id):debug("No sectors to add/configure") end --activate the Hound system hound:systemOn() end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- local function createSystems(loadUnits, atMissionStart) veaf.loggers.get(veafHoundElint.Id):debug(string.format("createSystems(%s, %s)", veaf.p(loadUnits), veaf.p(atMissionStart))) veafHoundElint.redHound = HoundElint:create(coalition.side.RED) veafHoundElint.redHound.name = "RED Hound" veafHoundElint.blueHound = HoundElint:create(coalition.side.BLUE) veafHoundElint.blueHound.name = "BLUE Hound" if loadUnits then initializeHoundSystem(coalition.side.RED, veafHoundElint.redParameters, atMissionStart) initializeHoundSystem(coalition.side.BLUE, veafHoundElint.blueParameters, atMissionStart) end end -- reset the Hound networks and rebuild them. Useful when a dynamic combat zone is deactivated function veafHoundElint.reinitialize(delay) veaf.loggers.get(veafHoundElint.Id):debug(string.format("reinitialize(%s)", veaf.p(delay))) if not veafHoundElint.reinitializeTaskID then if delay then veafHoundElint.reinitializeTaskID = mist.scheduleFunction(veafHoundElint._reinitialize , nil, veafHoundElint.DelayForStartup) end end end function veafHoundElint._reinitialize() veaf.loggers.get(veafHoundElint.Id):debug(string.format("_reinitialize()")) if not veafHoundElint.initialized then return false end if veafHoundElint.redHound then veafHoundElint.redHound:systemOff() end if veafHoundElint.blueHound then veafHoundElint.blueHound:systemOff() end createSystems(true, false) if veafHoundElint.reinitializeTaskID then veafHoundElint.reinitializeTaskID = nil end end function veafHoundElint.hasSectorCallsign(parameters) return parameters.callsign and tostring(parameters.callsign) end function veafHoundElint.hasTransmitterUnit(parameters) return parameters and parameters.transmitterUnit and tostring(parameters.transmitterUnit) and Group.getByName(parameters.transmitterUnit) end function veafHoundElint.hasAtis(parameters) return parameters and parameters.atis end function veafHoundElint.hasController(parameters) return parameters and parameters.controller end function veafHoundElint.hasControllerVoice(parameters) return parameters and parameters.controller and parameters.controller.voiceEnabled end function veafHoundElint.hasNotifier(parameters) return parameters and parameters.notifier end function veafHoundElint.hasNoTTS(parameters) return parameters and parameters.disableTTS end function veafHoundElint.hasNoAlerts(parameters) return parameters and parameters.disableAlerts end function veafHoundElint.initialize(prefix, red, blue) veafHoundElint.prefix = prefix -- if nil, all capable units will be set as Elint platforms veafHoundElint.redParameters = red or {} veafHoundElint.blueParameters = blue or {} veaf.loggers.get(veafHoundElint.Id):info("Initializing module") veaf.loggers.get(veafHoundElint.Id):debug(string.format("red=%s",veaf.p(red))) veaf.loggers.get(veafHoundElint.Id):debug(string.format("blue=%s",veaf.p(blue))) -- prepare the list of units supported by Hound Elint for platformType, platformData in pairs(HOUND.DB.Platform[Object.Category.STATIC]) do veafHoundElint.elintUnitsTypes[platformType] = true end for platformType, platformData in pairs(HOUND.DB.Platform[Object.Category.UNIT]) do veafHoundElint.elintUnitsTypes[platformType] = true end veaf.loggers.get(veafHoundElint.Id):trace(string.format("veafHoundElint.elintUnitsTypes=%s",veaf.p(veafHoundElint.elintUnitsTypes))) veafHoundElint.initialized = true veaf.loggers.get(veafHoundElint.Id):info(string.format("Loading units")) createSystems(true, true) veaf.loggers.get(veafHoundElint.Id):info(string.format("Hound Elint has been initialized")) end veaf.loggers.get(veafHoundElint.Id):info(string.format("Loading version %s", veafHoundElint.Version)) HOUND.Utils.Marker._MarkId = 1235634 -- select a less obvious marker id range that `9999` ------------------ END script veafHoundElintHelper.lua ------------------ ------------------ START script veafMissileGuardian.lua ------------------ ------------------------------------------------------------------ -- VEAF missile guardian functions for DCS World -- By zip (2020) -- -- Features: -- --------- -- * GUARDIAN objects that are configured to warn and protect specific units (helos, airplanes) of weapons fired in their direction -- * ANGEL objects following these weapons, warning their targets (distance, aspect, danger) and optionnaly destroy weapons in the air before impact (training, protection) -- * Works with all current and future maps (Caucasus, NTTR, Normandy, PG, ...) -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafMissileGuardian = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafMissileGuardian.Id = "MISSILEGUARDIAN" --- Version. veafMissileGuardian.Version = "0.0.1" -- trace level, specific to this module --veafMissileGuardian.LogLevel = "trace" veaf.loggers.new(veafMissileGuardian.Id, veafMissileGuardian.LogLevel) --- Number of seconds between each check of the WIDE ZONE watchdog function veafMissileGuardian.SecondsBetweenWideZoneWatchdogChecks = 5 --- Number of seconds between each check of the DANGER ZONE watchdog function veafMissileGuardian.SecondsBetweenDangerZoneWatchdogChecks = 0.5 veafMissileGuardian.RadioMenuName = "GUARDIAN" veafMissileGuardian.RemoteCommandParser = "([[a-zA-Z0-9]+)%s?([^%s]*)%s?(.*)" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Radio menus paths veafMissileGuardian.rootPath = nil ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafMG_Weapon object ; represents a weapon in flight, detected by a Guardian and managed by an Protector ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafMG_Weapon = { -- technical name name = nil, -- DCS weapon object dcsWeapon = nil, -- shooter shooter = nil, -- shooter name (if the shooter unit gets destroyed, keep its name) shooterName = nil, } VeafMG_Weapon.__index = VeafMG_Weapon function VeafMG_Weapon:new() veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Weapon:new()")) local self = setmetatable({}, VeafMG_Weapon) self.name = nil self.dcsWeapon = nil self.shooter = nil self.shooterName = nil return self end function VeafMG_Weapon:copy() local copy = VeafMG_Weapon:new() -- copy the attributes copy.name = self.name copy.dcsWeapon = self.dcsWeapon copy.shooter = self.shooter copy.shooterName = self.shooterName return copy end --- --- setters and getters --- function VeafMG_Weapon:setName(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Weapon.setName([%s])",value or "")) self.name = value return self end function VeafMG_Weapon:getName() return self.name end function VeafMG_Weapon:setDcsWeapon(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Weapon[%s].setDcsWeapon()",self:getName() or "")) self.dcsWeapon = value if self.dcsWeapon then self.shooter = self.dcsWeapon:getLauncher() self.shooterName = veafMissileGuardian.getUnitName(self.shooter) end return self end function VeafMG_Weapon:getDcsWeapon() return self.dcsWeapon end function VeafMG_Weapon:getShooter() return self.shooter end function VeafMG_Weapon:getShooterName() return self.shooterName end --- --- other methods --- function VeafMG_Weapon:getCurrentPosition() local _result = nil if self:getDcsWeapon() then _result = self:getDcsWeapon():getPoint() end return _result end function VeafMG_Weapon:getCurrentTarget() local _result = nil if self:getDcsWeapon() then _result = self:getDcsWeapon():getTarget() end return _result end function VeafMG_Weapon:getCurrentEnergy() local _result = nil if self:getDcsWeapon() then local _mass = 250 -- let's say the missile weights 250kg local _vector = self:getDcsWeapon():getVelocity() local _absVelocity = mist.vec.mag(_vector) local _kinetic = (_mass / 2) * _absVelocity * _absVelocity local _alt = self:getDcsWeapon():getPoint().z local _potential = _mass * 9.81 * _alt _result = _kinetic + _potential end return _result end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafMG_Guardian object -- The Guardian observes units and reacts if one of them fires a weapon by encapsulating it into a VeafMG_Weapon and passing it to a VeafMG_Protector for observation ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafMG_Guardian = { -- technical name name = nil, -- human-friendly name friendlyName = nil, -- list of units that should be protected from missiles protectedUnits = nil, -- polygon describing a zone when the units should be to be protected protectedZone = nil, } VeafMG_Guardian.__index = VeafMG_Guardian VeafMG_Guardian.WARNING_MESSAGE = "Warning, %s : you've been attacked by %s and a missile is in the air" VeafMG_Guardian.WARNING_MESSAGE_TIME = 10 function VeafMG_Guardian:new() veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Guardian:new()")) local self = setmetatable({}, VeafMG_Guardian) self.name = nil self.friendlyName = nil self.protectedUnits = {} self.protectedZone = {} return self end function VeafMG_Guardian:copy() local copy = VeafMG_Guardian:new() -- copy the attributes copy.name = self.name copy.friendlyName = self.friendlyName -- deep copy the collections copy.protectedUnits = {} for unitName, value in pairs(self.protectedUnits) do copy.protectedZone[unitName] = value end copy.protectedZone = {} for _, value in pairs(self.protectedZone) do table.insert(copy.protectedZone, value) end return copy end --- --- setters and getters --- function VeafMG_Guardian:setName(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Guardian[]:setName([%s])",value or "")) self.name = value return self end function VeafMG_Guardian:getName() return self.name end function VeafMG_Guardian:setFriendlyName(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Guardian[%s]:setFriendlyName()",self:getName() or "")) self.friendlyName = value return self end function VeafMG_Guardian:getFriendlyName() return self.friendlyName end function VeafMG_Guardian:addProtectedUnit(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Guardian[%s]:addProtectedUnit()",self:getName() or "")) if type(value) ~= "string" then value = value:getName() end self.protectedUnits[value] = "protected" return self end function VeafMG_Guardian:setProtectedZone(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Guardian[%s]:setProtectedZone()",self:getName() or "")) self.protectedZone = value return self end --- --- other methods --- --- event handler function VeafMG_Guardian:onEvent(event) -- only react to S_EVENT_SHOT events if event and event.id == world.event.S_EVENT_SHOT then veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Protector:onEvent(S_EVENT_SHOT) : %s", veaf.p(event))) if event.weapon then -- check if the target is one of the protected units local _target = event.weapon:getTarget() if _target then local _targetName = _target:getName() veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_targetName = %s", veaf.p(_targetName))) if self.protectedUnits[_targetName] then -- check if the target is in the protected zone local _inZone = mist.pointInPolygon(_target:getPoint(), self.protectedZone) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_inZone = %s", veaf.p(_inZone))) if _inZone then -- encapsulate the event weapon local _weapon = VeafMG_Weapon:new():setDcsWeapon(event.weapon) -- message the target unit local _groupId = _target:getGroup():getID() local _playername = _target:getPlayerName() if _playername then veaf.loggers.get(veafMissileGuardian.Id):debug(string.format("Issuing a warning to unit %s", veaf.p(_playername))) trigger.action.outTextForGroup(_groupId, string.format(VeafMG_Guardian.WARNING_MESSAGE, _playername, _weapon:getShooterName()), VeafMG_Guardian.WARNING_MESSAGE_TIME) end -- pass the weapon to the large-scale protector veafMissileGuardian.getLargeScaleProtector():setWeapon(_weapon) end end end end end end function VeafMG_Guardian:start() -- register event handler world.addEventHandler(self) end function VeafMG_Guardian:stop() -- deregister event handler world.removeEventHandler(self) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafMG_Protector object -- Responsible for observing and reporting VeafMG_Weapons behavior, and protecting the registered units by destroying the weapons in flight when needed ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafMG_Protector = { -- technical name name = nil, -- seconds between watchdog checks secondsBetweenWatchdogChecks = nil, -- weapon we're trying to protect from weapon = nil, } VeafMG_Protector.__index = VeafMG_Protector function VeafMG_Protector:new() veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Protector:new()")) local self = setmetatable({}, VeafMG_Protector) self.name = nil self.secondsBetweenWatchdogChecks = nil self.weapon = nil return self end function VeafMG_Protector:copy() local copy = VeafMG_Protector:new() -- copy the attributes copy.name = self.name copy.secondsBetweenWatchdogChecks = self.secondsBetweenWatchdogChecks copy.weapon = self.weapon -- deep copy the collections -- copy.parameters = {} -- for name, value in pairs(self.parameters) do -- veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("copying parameter %s : ",tostring(name))) -- copy.parameters[name]=value -- end return copy end --- --- setters and getters --- function VeafMG_Protector:setName(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Protector[]:setName([%s])",value or "")) self.name = value return self end function VeafMG_Protector:getName() return self.name end function VeafMG_Protector:setSecondsBetweenWatchdogChecks(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Protector[%s]:setSecondsBetweenWatchdogChecks()",self:getName() or "")) self.secondsBetweenWatchdogChecks = value return self end function VeafMG_Protector:getSecondsBetweenWatchdogChecks() return self.secondsBetweenWatchdogChecks end function VeafMG_Protector:setWeapon(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Protector[%s]:setWeapon()",self:getName() or "")) self.weapon = value return self end --- --- other methods --- --- function VeafMG_Protector:start() -- schedule the watchdog function end function VeafMG_Protector:stop() -- unschedule the watchdog function end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- local functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMissileGuardian.getUnitName(unit) return unit:getName() -- TODO make this useful (add the player name if possible) end function veafMissileGuardian.getLargeScaleProtector() -- TODO end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- global functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- add a new guardian function veafMissileGuardian.AddGuardian(guardian) veaf.loggers.get(veafMissileGuardian.Id):debug(string.format("veafMissileGuardian.AddGuardian([%s])",guardian:getName() or "")) return guardian end -- activate a guardian function veafMissileGuardian.ActivateGuardian(name, silent) veaf.loggers.get(veafMissileGuardian.Id):debug(string.format("veafMissileGuardian.ActivateGuardian([%s])",name or "")) local guardian = veafMissileGuardian.GetGuardian(name) local result = guardian:activate(silent) if not silent and not guardian:isSilent() then if result then trigger.action.outText("VeafMG_Guardian "..guardian:getFriendlyName().." has been activated.", 10) else trigger.action.outText("VeafMG_Guardian "..guardian:getFriendlyName().." was already active.", 10) end end veafMissileGuardian.buildRadioMenu() end -- desactivate a guardian function veafMissileGuardian.DesactivateGuardian(name, silent) veaf.loggers.get(veafMissileGuardian.Id):debug(string.format("veafMissileGuardian.DesactivateGuardian([%s])",name or "")) local guardian = veafMissileGuardian.GetGuardian(name) local result = guardian:desactivate(silent) if not silent and not guardian:isSilent() then if result then trigger.action.outText("VeafMG_Guardian "..guardian:getFriendlyName().." has been desactivated.", 10) else trigger.action.outText("VeafMG_Guardian "..guardian:getFriendlyName().." was already inactive.", 10) end end veafMissileGuardian.buildRadioMenu() end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMissileGuardian._buildMissionRadioMenu(menu, title, element) local missions = element.missions if #missions == 1 then -- one simple mission local mission = missions[1] if mission:isActive() then title = "* "..title end mission.radioRootPath = veafRadio.addSubMenu(title, menu) mission:updateRadioMenu(true) else -- group by skill and scale veaf.loggers.get(veafMissileGuardian.Id):trace("group by skill and scale") local skills = {} for _, mission in pairs(missions) do local regex = ("^([^/]+)/([^/]+)/(%d+)$") local name, skill, scale = mission:getName():match(regex) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("missionName=[%s], name=%s, skill=%s, scale=%s", tostring(mission:getName()), tostring(name), tostring(skill), tostring(scale))) if not skills[skill] then skills[skill] = {} end skills[skill][scale] = mission end veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("skills=%s", veaf.p(skills))) -- create the radio menus local title = title if element.activeGroups then title = "* "..title end local missionPath = veafRadio.addSubMenu(title, menu) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format(" %s", title)) local skillsNames = {} for skill, _ in pairs(skills) do table.insert(skillsNames, skill) end table.sort(skillsNames) for _, skill in pairs(skillsNames) do local scales = skills[skill] local skillTitle = skill if element.activeGroups and element.activeGroups[skill] then skillTitle = "* "..skillTitle end local skillPath = veafRadio.addSubMenu(skillTitle, missionPath) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format(" %s", skill)) local scalesNames = {} for scale, _ in pairs(scales) do table.insert(scalesNames, scale) end table.sort(scalesNames) for _, scale in pairs(scalesNames) do local mission = scales[scale] local scaleTitle = "scale "..scale if element.activeGroups and element.activeGroups[skill] and element.activeGroups[skill][scale] then scaleTitle = "* "..scaleTitle end local scalePath = veafRadio.addSubMenu(scaleTitle, skillPath) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format(" %s", scale)) mission.radioRootPath = scalePath mission:updateRadioMenu(true) end end end end --- Build the initial radio menu function veafMissileGuardian.buildRadioMenu() veaf.loggers.get(veafMissileGuardian.Id):debug("buildRadioMenu()") if veafMissileGuardian.rootPath then veafRadio.clearSubmenu(veafMissileGuardian.rootPath) else veafMissileGuardian.rootPath = veafRadio.addMenu(veafMissileGuardian.RadioMenuName) end if not(veafRadio.skipHelpMenus) then veafRadio.addCommandToSubmenu("HELP", veafMissileGuardian.rootPath, veafMissileGuardian.help, nil, veafRadio.USAGE_ForGroup) end veafRadio.refreshRadioMenu() end function veafMissileGuardian.listGuardians() -- sort the missions alphabetically local sortedMissions = {} table.sort(sortedMissions) local text = 'List of all available guardians:\n' for _, missionName in pairs(sortedMissions) do text = text .. " - " .. missionName .. "\n" end trigger.action.outText(text, 20) end function veafMissileGuardian.listActiveMissions() -- sort the missions alphabetically local sortedMissions = {} for _, mission in pairs(veafMissileGuardian.missionsDict) do if mission:isActive() then table.insert(sortedMissions, mission:getName() .. ' : ' .. mission:getRemainingEnemiesString()) end end table.sort(sortedMissions) local text = 'No active combat mission !' if #sortedMissions > 0 then text = 'List of active combat missions:\n' for _, missionName in pairs(sortedMissions) do text = text .. " - " .. missionName .. "\n" end end trigger.action.outText(text, 20) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- execute command from the remote interface function veafMissileGuardian.executeCommandFromRemote(parameters) veaf.loggers.get(veafMissileGuardian.Id):debug(string.format("veafMissileGuardian.executeCommandFromRemote()")) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_pilot= %s", veaf.p(_pilot))) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName))) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_unitName= %s", veaf.p(_unitName))) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_command= %s", veaf.p(_command))) if not _pilot or not _command then return false end if _command then -- parse the command local _action, _missionName, _parameters = _command:match(veafMissileGuardian.RemoteCommandParser) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_action=%s",veaf.p(_action))) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_guardianName=%s",veaf.p(_missionName))) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_parameters=%s",veaf.p(_parameters))) if _action and _action:lower() == "list" then veaf.loggers.get(veafMissileGuardian.Id):info(string.format("[%s] is listing air missions)",veaf.p(_pilot.name))) veafMissileGuardian.listAvailableMissions() return true elseif _action and _action:lower() == "start" and _missionName then local _silent = _parameters and _parameters:lower() == "silent" veaf.loggers.get(veafMissileGuardian.Id):info(string.format("[%s] is starting air mission [%s] %s)",veaf.p(_pilot.name), veaf.p(_missionName), veaf.p(_parameters))) veafMissileGuardian.ActivateMission(_missionName, _silent) return true elseif _action and _action:lower() == "stop" then local _silent = _parameters and _parameters:lower() == "silent" veaf.loggers.get(veafMissileGuardian.Id):info(string.format("[%s] is stopping air mission [%s] %s)",veaf.p(_pilot.name), veaf.p(_missionName), veaf.p(_parameters))) veafMissileGuardian.DesactivateMission(_missionName, _silent) return true end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMissileGuardian.initialize() veaf.loggers.get(veafMissileGuardian.Id):info("Initializing module") veafMissileGuardian.buildRadioMenu() veafMissileGuardian.dumpMissionsList(veaf.config.MISSION_EXPORT_PATH) end veaf.loggers.get(veafMissileGuardian.Id):info(string.format("Loading version %s", veafMissileGuardian.Version)) ------------------ END script veafMissileGuardian.lua ------------------ ------------------ START script veafMove.lua ------------------ ------------------------------------------------------------------ -- VEAF move units for DCS World -- By mitch (2018) -- -- Features: -- --------- -- * Listen to marker change events and execute move commands, with optional parameters -- * Possibilities : -- * - move a specific group to a marker point, at a specific speed -- * - create a new tanker flightplan, moving a specific tanker group -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ --- veafMove Table. veafMove = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafMove.Id = "MOVE" --- Version. veafMove.Version = "1.10.0" -- trace level, specific to this module --veafMove.LogLevel = "trace" veaf.loggers.new(veafMove.Id, veafMove.LogLevel) --- Key phrase to look for in the mark text which triggers the command. veafMove.Keyphrase = "_move" veafMove.RadioMenuName = "MOVE" veafMove.tankerMissionParameters = { ["A-10C"] = {speed=250, alt=12000}, ["A-10C_2"] = {speed=250, alt=12000}, ["AV8BNA"] = {speed=350, alt=18000}, ["F-14A"] = {speed=400, alt=22000}, ["F-14A-135-GR"] = {speed=400, alt=22000}, ["F-14B"] = {speed=400, alt=22000}, ["F-15C"] = {speed=400, alt=22000}, ["F-15E"] = {speed=400, alt=22000}, ["F-16A"] = {speed=400, alt=22000}, ["F-16A MLU"] = {speed=400, alt=22000}, ["F-16C bl.50"] = {speed=400, alt=22000}, ["F-16C bl.52d"] = {speed=400, alt=22000}, ["F-16C_50"] = {speed=400, alt=22000}, ["F-4E"] = {speed=300, alt=18000}, ["F/A-18A"] = {speed=400, alt=22000}, ["F/A-18C"] = {speed=400, alt=22000}, ["FA-18C_hornet"] = {speed=400, alt=22000}, ["JF-17"] = {speed=400, alt=22000}, ["M-2000C"] = {speed=400, alt=22000}, ["MiG-29K"] = {speed=400, alt=22000}, ["MiG-31"] = {speed=400, alt=22000}, ["Mirage 2000-5"] = {speed=400, alt=22000}, ["Su-24M"] = {speed=400, alt=22000}, ["Su-24MR"] = {speed=400, alt=22000}, ["Su-33"] = {speed=400, alt=22000}, ["Su-34"] = {speed=400, alt=22000}, ["Tornado GR4"] = {speed=400, alt=22000}, } ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafMove.rootPath = nil --- Initial Marker id. veafMove.markid = 20000 traceMarkerId = 6548 debugMarkers = {} veafMove.Tankers = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function executed when a mark has changed. This happens when text is entered or changed. function veafMove.onEventMarkChange(eventPos, event) if veafMove.executeCommand(eventPos, event.text) then -- Delete old mark. veaf.loggers.get(veafMove.Id):trace(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafMove.executeCommand(eventPos, eventText, bypassSecurity) -- Check if marker has a text and the veafMove.keyphrase keyphrase. if eventText ~= nil and eventText:lower():find(veafMove.Keyphrase) then -- Analyse the mark point text and extract the keywords. local options = veafMove.markTextAnalysis(eventText) local result = false if options then -- Check options commands if options.moveGroup then result = veafMove.moveGroup(eventPos, options.groupName, options.speed, options.altitude) elseif options.moveTanker then result = veafMove.moveTanker(eventPos, options.groupName, options.speed, options.altitude, options.hdg, options.distance, options.teleport, options.silent) elseif options.changeTanker then result = veafMove.changeTanker(eventPos, options.speed, options.altitude) elseif options.moveAfac then result = veafMove.moveAfac(eventPos, options.groupName, options.speed, options.altitude, options.hdg, options.immortal) end else -- None of the keywords matched. return false end return result end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Analyse the mark text and extract keywords. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Extract keywords from mark text. function veafMove.markTextAnalysis(text) -- Option parameters extracted from the mark text. local switch = {} switch.moveGroup = false switch.moveTanker = false switch.changeTanker = false switch.moveAfac = false -- the name of the group to move ; mandatory switch.groupName = "" -- speed in knots switch.speed = -1 -- defaults to original speed -- tanker refuel leg altitude in feet switch.altitude = -1 -- defaults to tanker original altitude -- tanker refuel leg heading in degrees switch.hdg = nil -- defaults to original heading -- option to set AFAC to immortal switch.immortal = false -- tanker refuel leg distance in degrees switch.distance = nil -- defaults to original distance -- if true, teleport the tanker instead of simply making it move switch.teleport = false -- if false, Named Points will be created when moving the tankers switch.silent = false -- Check for correct keywords. if text:lower():find(veafMove.Keyphrase .. " group") then switch.moveGroup = true switch.speed = 20 elseif text:lower():find(veafMove.Keyphrase .. " tankermission") then switch.changeTanker = true switch.speed = -1 switch.altitude = -1 elseif text:lower():find(veafMove.Keyphrase .. " tanker") then switch.moveTanker = true switch.speed = -1 switch.altitude = -1 elseif text:lower():find(veafMove.Keyphrase .. " afac") then switch.moveAfac = true switch.speed = 150 switch.altitude = 15000 else return nil end -- keywords are split by "," local keywords = veaf.split(text, ",") for _, keyphrase in pairs(keywords) do -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. local str = veaf.breakString(veaf.trim(keyphrase), " ") local key = str[1] local val = str[2] if key:lower() == "name" then -- Set group name veaf.loggers.get(veafMove.Id):debug(string.format("Keyword name = %s", val)) switch.groupName = val end if key:lower() == "speed" or key:lower() == "spd" then -- Set speed. veaf.loggers.get(veafMove.Id):debug(string.format("Keyword speed = %d", val)) local nVal = tonumber(val) switch.speed = nVal end if key:lower() == "heading" or key:lower() == "hdg" then -- Set heading. veaf.loggers.get(veafMove.Id):debug(string.format("Keyword hdg = %d", val)) local nVal = tonumber(val) switch.hdg = nVal end if key:lower() == "distance" or key:lower() == "dist" then -- Set distance. veaf.loggers.get(veafMove.Id):debug(string.format("Keyword distance = %d", val)) local nVal = tonumber(val) switch.distance = nVal end if key:lower() == "alt" or key:lower() == "altitude" then -- Set altitude. veaf.loggers.get(veafMove.Id):debug(string.format("Keyword alt = %d", val)) local nVal = tonumber(val) switch.altitude = nVal end if key:lower() == "teleport" then veaf.loggers.get(veafMove.Id):trace("Keyword teleport found") switch.teleport = true end if key:lower() == "silent" then veaf.loggers.get(veafMove.Id):trace("Keyword silent found") switch.silent = true end if key:lower() == "immortal" then veaf.loggers.get(veafMove.Id):trace("Keyword immortal found") switch.immortal = true end end -- check mandatory parameter "group" if not(switch.groupName) then return nil end return switch end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Group move command ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------ -- veafMove.moveGroup -- @param point eventPos -- @param string groupName the group name to move on -- @param float speed in knots ------------------------------------------------------------------------------ function veafMove.moveGroup(eventPos, groupName, speed, altitude) veaf.loggers.get(veafMove.Id):debug("veafMove.moveGroup(groupName = " .. groupName .. ", speed = " .. speed .. ", altitude=".. altitude) veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.moveGroup: eventPos x=%.1f z=%.1f", eventPos.x, eventPos.z)) local result = veaf.moveGroupTo(groupName, eventPos, speed, altitude) if not(result) then trigger.action.outText(groupName .. ' not found for move group command' , 10) end return result end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Change tanker mission parameters ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMove.changeTanker(eventPos, speed, alt) veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.changeTanker(speed=%s, alt=%s)", tostring(speed), tostring(alt))) veaf.loggers.get(veafMove.Id):trace(string.format("eventPos=%s",veaf.p(eventPos))) veaf.loggers.get(veafMove.Id):cleanupMarkers(debugMarkers) local tankerUnit = nil local units = veaf.findUnitsInCircle(eventPos, 2000, false) veaf.loggers.get(veafMove.Id):trace(string.format("units=%s", veaf.p(units))) if units then for name, _ in pairs(units) do -- try and find a tanker unit local unit = Unit.getByName(name) if unit and unit:getDesc()["attributes"]["Tankers"] then tankerUnit = unit break end end end if not tankerUnit then veaf.loggers.get(veafMove.Id):warn("Cannot find tanker unit around marker") trigger.action.outText("Cannot find tanker unit around marker" , 10) return false end local tankerGroup = tankerUnit:getGroup() local tankerGroupName = tankerGroup:getName() local tankerData = veaf.getGroupData(tankerGroupName) if not(tankerData) then local text = "Cannot move tanker " .. tankerGroupName .. " ; cannot find group data" veaf.loggers.get(veafMove.Id):info(text) trigger.action.outText(text) return end local route = veaf.findInTable(tankerData, "route") local points = veaf.findInTable(route, "points") if points then veaf.loggers.get(veafMove.Id):trace("found a " .. #points .. "-points route for tanker " .. tankerGroupName) -- modify the last 3 points local idxPoint1 = #points-2 local idxPoint2 = #points-1 local idxPoint3 = #points -- point1 is the point where the tanker mission starts ; we'll change the speed and altitude local point1 = points[idxPoint1] veaf.loggers.get(veafMove.Id):trace("found point1") -- set speed if speed > -1 then point1.speed = speed/1.94384 -- in m/s else speed = point1.speed*1.94384 -- in knots end -- set altitude if alt > -1 then point1.alt = alt * 0.3048 -- in meters else alt = point1.alt / 0.3048 -- in feet end veaf.loggers.get(veafMove.Id):trace(string.format("newPoint1=%s",veaf.p(point1))) -- point 2 is the start of the tanking Orbit ; we'll change the speed and altitude local point2 = points[idxPoint2] veaf.loggers.get(veafMove.Id):trace("found point2") local foundOrbit = false local task1 = veaf.findInTable(point2, "task") if task1 then local tasks = task1.params.tasks if (tasks) then veaf.loggers.get(veafMove.Id):trace("found %s tasks", veaf.p(#tasks)) for j, task in pairs(tasks) do veaf.loggers.get(veafMove.Id):trace("found task #%s", veaf.p(j)) if task.params then veaf.loggers.get(veafMove.Id):trace("has .params") if task.id and task.id == "Orbit" then veaf.loggers.get(veafMove.Id):debug("Found a ORBIT task for tanker " .. tankerGroupName) foundOrbit = true if speed > -1 then task.params.speed = speed/1.94384 -- in m/s point2.speed = speed/1.94384 -- in m/s end if alt > -1 then task.params.altitude = alt * 0.3048 -- in meters point2.alt = alt * 0.3048 -- in meters end end end end end end if not foundOrbit then local text = "Cannot set tanker " .. tankerGroupName .. " parameters because it has no ORBIT task defined" veaf.loggers.get(veafMove.Id):info(text) trigger.action.outText(text) return end -- point 3 is the end of the tanking Orbit ; we'll change the speed and altitude local point3 = points[idxPoint3] veaf.loggers.get(veafMove.Id):trace("found point3") -- change speed if speed > -1 then point3.speed = speed/1.94384 -- in m/s end -- change altitude if alt > -1 then point3.alt = alt * 0.3048 -- in meters end veaf.loggers.get(veafMove.Id):trace("newpoint3="..veaf.p(point3)) -- replace whole mission veaf.loggers.get(veafMove.Id):debug("Resetting changed tanker mission") -- replace the mission local mission = { id = 'Mission', params = tankerData } local controller = tankerGroup:getController() controller:setTask(mission) local msg = string.format("Set tanker %s to %d kn (ground) at %d ft", tankerGroupName, speed, alt) veaf.loggers.get(veafMove.Id):info(msg) trigger.action.outText(msg , 10) return true else return false end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Tanker move command ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMove.moveTanker(eventPos, groupName, speed, alt, hdg, distance, teleport, silent) veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.moveTanker(groupName=%s, speed=%s, alt=%s, hdg=%s, distance=%s)",tostring(groupName), tostring(speed), tostring(alt), tostring(hdg), tostring(distance))) veaf.loggers.get(veafMove.Id):cleanupMarkers(debugMarkers) veaf.loggers.get(veafMove.Id):trace(string.format("eventPos=%s",veaf.p(eventPos))) local FIRSTPOINT_DISTANCE_SECONDS = 60 -- seconds to fly to WP1 local unitGroup = Group.getByName(groupName) if unitGroup == nil then veaf.loggers.get(veafMove.Id):info(groupName .. ' not found for move tanker command') trigger.action.outText(groupName .. ' not found for move tanker command' , 10) return false end local tankerData = veaf.getGroupData(groupName) if not(tankerData) then local text = "Cannot move tanker " .. groupName .. " ; no group data" veaf.loggers.get(veafMove.Id):info(text) trigger.action.outText(text) return false end veaf.loggers.get(veafMove.Id):trace("tankerData : %s", veaf.p(tankerData)) local route = veaf.findInTable(tankerData, "route") local points = veaf.findInTable(route, "points") if points then veaf.loggers.get(veafMove.Id):trace("found a " .. #points .. "-points route for tanker " .. groupName) -- modify the last 3 points local idxPoint1 = #points-2 local idxPoint2 = #points-1 local idxPoint3 = #points local point1 = points[idxPoint1] veaf.loggers.get(veafMove.Id):trace(string.format("point1=%s",veaf.p(point1))) local point2 = points[idxPoint2] veaf.loggers.get(veafMove.Id):trace(string.format("point2=%s",veaf.p(point2))) local point3 = points[idxPoint3] veaf.loggers.get(veafMove.Id):trace(string.format("point3=%s",veaf.p(point3))) -- if distance is not set, compute distance between point2 and point3 local distance = distance if distance == nil then distance = math.sqrt((point3.x - point2.x)^2+(point3.y - point2.y)^2) else -- convert distance to meters distance = distance * 1852 -- meters end -- if hdg is not set, compute heading between point2 and point3 local hdg = hdg if hdg == nil then hdg = veaf.headingBetweenPoints(point2, point3) else hdg = hdg * math.pi/180 end -- if speed is not set, use point2 speed local speed = speed if speed == nil or speed < 0 then speed = point2.speed else -- convert speed to m/s speed = speed/1.94384 end -- if alt is not set, use point2 altitude local alt = alt if alt == nil or alt < 0 then alt = point2.alt else -- convert altitude to meters alt = alt * 0.3048 -- meters end veaf.loggers.get(veafMove.Id):trace(string.format("distance=%s",veaf.p(distance))) veaf.loggers.get(veafMove.Id):trace(string.format("hdg=%s",veaf.p(hdg))) veaf.loggers.get(veafMove.Id):trace(string.format("speed=%s",veaf.p(speed))) veaf.loggers.get(veafMove.Id):trace(string.format("alt=%s",veaf.p(alt))) -- the first point in the refuel leg is based on the marker position local startLegPoint= { x=eventPos.x, y=eventPos.z, alt=alt, speed=speed } veaf.loggers.get(veafMove.Id):trace(string.format("startLegPoint=%s",veaf.p(startLegPoint))) if veafNamedPoints and not silent then veafNamedPoints.namePoint({x=startLegPoint.x, y=startLegPoint.alt, z=startLegPoint.y}, groupName .. " refuel start", unitGroup:getCoalition(), true) end -- compute the second point in the refuel leg based on desired heading and distance local endLegPoint= { x=startLegPoint.x, y=startLegPoint.y, alt=alt, speed=speed } veaf.loggers.get(veafMove.Id):trace(string.format("distance=%s",veaf.p(distance))) veaf.loggers.get(veafMove.Id):trace(string.format("hdg=%s",veaf.p(hdg))) endLegPoint.x = startLegPoint.x + distance * math.cos(hdg) endLegPoint.y = startLegPoint.y + distance * math.sin(hdg) veaf.loggers.get(veafMove.Id):trace(string.format("endLegPoint=%s",veaf.p(endLegPoint))) if veafNamedPoints and not silent then veafNamedPoints.namePoint({x=endLegPoint.x, y=endLegPoint.alt, z=endLegPoint.y}, groupName .. " refuel end", unitGroup:getCoalition(), true) end -- compute the point where the tanker should move in the opposite direction from the desired heading, at a standard distance local movePoint= { x=startLegPoint.x, y=startLegPoint.y, alt=alt, speed=speed } local teleportPoint= { x=startLegPoint.x, y=startLegPoint.y, alt=alt, speed=speed } local reverseHdg = hdg - math.pi if reverseHdg < 0 then reverseHdg = reverseHdg + math.pi*2 end veaf.loggers.get(veafMove.Id):trace(string.format("reverseHdg=%s",veaf.p(reverseHdg))) movePoint.x = startLegPoint.x + 1.5 * speed * FIRSTPOINT_DISTANCE_SECONDS * math.cos(reverseHdg) movePoint.y = startLegPoint.y + 1.5 * speed * FIRSTPOINT_DISTANCE_SECONDS * math.sin(reverseHdg) teleportPoint.x = startLegPoint.x + 3 * speed * FIRSTPOINT_DISTANCE_SECONDS * math.cos(reverseHdg) teleportPoint.y = startLegPoint.y + 3 * speed * FIRSTPOINT_DISTANCE_SECONDS * math.sin(reverseHdg) veaf.loggers.get(veafMove.Id):trace(string.format("movePoint=%s",veaf.p(movePoint))) -- set point1 to the computed movePoint point1.x = movePoint.x point1.y = movePoint.y point1.alt = movePoint.alt point1.speed = movePoint.speed veaf.loggers.get(veafMove.Id):trace(string.format("newPoint1=%s",veaf.p(point1))) -- set point2 to the start of the tanking Orbit (startLegPoint) local foundOrbit = false local task1 = veaf.findInTable(point2, "task") if task1 then local tasks = task1.params.tasks if (tasks) then veaf.loggers.get(veafMove.Id):trace("found " .. #tasks .. " tasks") for j, task in pairs(tasks) do veaf.loggers.get(veafMove.Id):trace(string.format("found task #%s", veaf.p(j))) if task.params then veaf.loggers.get(veafMove.Id):trace("has .params") if task.id and task.id == "Orbit" then veaf.loggers.get(veafMove.Id):debug("Found a ORBIT task for tanker " .. groupName) foundOrbit = true task.params.speed = speed task.params.altitude = alt end end end end end if not foundOrbit then local text = "Cannot move tanker " .. groupName .. " because it has no ORBIT task defined" veaf.loggers.get(veafMove.Id):info(text) trigger.action.outText(text, 10) return false end point2.x = startLegPoint.x point2.y = startLegPoint.y point2.alt = startLegPoint.alt point2.speed = startLegPoint.speed veaf.loggers.get(veafMove.Id):trace(string.format("newPoint2=%s",veaf.p(point2))) -- set point2 to the end of the tanking Orbit (endLegPoint) point3.x = endLegPoint.x point3.y = endLegPoint.y point3.alt = endLegPoint.alt point3.speed = endLegPoint.speed veaf.loggers.get(veafMove.Id):trace("newpoint3="..veaf.p(point3)) --actually move the group local delay = 0 -- teleport if the option is set if teleport then veaf.loggers.get(veafMove.Id):debug("Teleport the tanker") local vars = { groupName = groupName, point = teleportPoint, action = "teleport"} local grp = mist.teleportToPoint(vars) unitGroup = Group.getByName(groupName) veafMove.teleportEscort(groupName, movePoint, teleportPoint) delay = 1 end veaf.loggers.get(veafMove.Id):debug(string.format("Resetting moved tanker mission in %d seconds", delay)) veafMove.replaceMission(unitGroup, tankerData, delay) return true else return false end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Escort move method, only called internally -- @param escorted_groupName, string, corresponds to the groupname of the aicraft being escorted -- @param movePoint, vec3 + speed, corresponds to the first waypoint that the escorted aircraft will take after it was moved -- @param teleportPoint, vec3 + speed, corresponds to the waypoint on which the escorted aircraft is teleported to, this is required ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMove.teleportEscort(escorted_groupName, movePoint, teleportPoint) --verify existence of the escorted aircraft local unitGroup = Group.getByName(escorted_groupName) if not unitGroup then veaf.loggers.get(veafMove.Id):info("Cannot move the escort of " .. escorted_groupName .. " ; this groupName does not correspond to any aircraft") return false end --verify the existence of the escort and proper configuration local escortedId = Group.getID(unitGroup) --this and only this serves as a groupID, what is given in EscortData does not correspond on the DCS side local groupName_escort = escorted_groupName .. " escort" --standardized escort groupName local unitGroup_escort = Group.getByName(groupName_escort) local escort_flag = false --indicates the go ahead for teleport/replaceMission calls local route_escort = {} local points_escort = {} local idxPoint1_escort = nil local idxPoint2_escort = nil local point1_escort = {} local point2_escort = {} local task2_escort = {} local tasks_escort = {} local task_escort = {} local EscortData = {} if unitGroup_escort ~= nil then EscortData = veaf.getGroupData(groupName_escort) if not(EscortData) then local text = "Cannot move Escort " .. groupName_escort .. " ; no group data" veaf.loggers.get(veafMove.Id):info(text) else veaf.loggers.get(veafMove.Id):trace("EscortData : %s", veaf.p(EscortData)) route_escort = veaf.findInTable(EscortData, "route") points_escort = veaf.findInTable(route_escort, "points") if points_escort then veaf.loggers.get(veafMove.Id):debug("Escort has WP") idxPoint1_escort = #points_escort-1 --second to last waypoint idxPoint2_escort = #points_escort --last waypoint where the escort has to be set up in the editor point1_escort = points_escort[idxPoint1_escort] point2_escort = points_escort[idxPoint2_escort] task2_escort = veaf.findInTable(point2_escort, "task") if task2_escort.params.tasks then veaf.loggers.get(veafMove.Id):debug("Last escort WP has tasks") tasks_escort = task2_escort.params.tasks for k, task in pairs(tasks_escort) do --if task.enabled and task.id and task.id == "Escort" and task.params and task.params.groupId == unitGroup_Id then --this line should be used to verify proper configuration of the escort but as it turns out the groupId stored in params has nothing to do with the groupId DCS needs for the escort task, use Group.getID(groupClass) instead to get the correct ID required for DCS, but no way to derive it from EscortData if task.enabled and task.id and task.id == "Escort" and task.params then veaf.loggers.get(veafMove.Id):trace("Found correct escort Tasking ! Extracted Escorted ID : %s", task.params.groupId) veaf.loggers.get(veafMove.Id):trace("Required escort ID : %s", escortedId) escort_flag = true task_escort=task --recover the escort task table to insert the "new" escorted ID, even though it's the same but it seems DCS destroys it after the escorted group respawns end end end end end else veaf.loggers.get(veafMove.Id):info(groupName_escort .. ' not found for move tanker escort command') end if not escort_flag then return false end --distances by which the escort is offseted from the escorted group in the map's referential, task_escort provides relative spacing local escort_offset = {} local hdg = veaf.headingBetweenPoints(teleportPoint, movePoint) escort_offset.x = (task_escort.params.pos.x*math.cos(hdg) - task_escort.params.pos.z*math.sin(hdg)) escort_offset.z = (task_escort.params.pos.x*math.sin(hdg) + task_escort.params.pos.z*math.cos(hdg)) local teleportPoint_escort = {} teleportPoint_escort.x = teleportPoint.x + escort_offset.x teleportPoint_escort.y = teleportPoint.y + escort_offset.z teleportPoint_escort.alt = teleportPoint.alt + task_escort.params.pos.y teleportPoint_escort.speed = teleportPoint.speed --Effectively waypoint 0, the AI will have to fly over it and in the editor it never poses a problem but in scripting the AI will do orbits to try and reach it --so it has to be offseted point1_escort.x = (teleportPoint.x + movePoint.x)/2 + escort_offset.x point1_escort.y = (teleportPoint.y + movePoint.y)/2 + escort_offset.z point1_escort.alt = movePoint.alt + task_escort.params.pos.y point1_escort.speed = movePoint.speed --Waypoint 1 where the escort tasking will come into play point2_escort.x = 2*point1_escort.x - teleportPoint.x - escort_offset.x point2_escort.y = 2*point1_escort.y - teleportPoint.y - escort_offset.z point2_escort.alt = movePoint.alt + task_escort.params.pos.y point2_escort.speed = movePoint.speed task_escort.params.groupId = escortedId --assign the new groupID within the old escort mission, only necessary after teleporting as the tanker's ID will have changed veaf.loggers.get(veafMove.Id):debug("Teleport the escort") local vars_escort = { groupName = groupName_escort, point = teleportPoint_escort, action = "teleport"} local grp_escort = mist.teleportToPoint(vars_escort) unitGroup_escort = Group.getByName(groupName_escort) veafMove.replaceMission(unitGroup_escort, EscortData) --this method appears to not work very well, the escort just doesn't defend the group --mist.goRoute(groupName_escort, route_escort) --works even worse, sends them to X=0, Z=0 return true end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- support method to replace the mission of moved aircraft (default delay of 1s for teleported aircraft) -- @param unitGroup, data returned by the Group.getByName(groupName) command -- @param missionData, data returned by the veaf.getGroupData(groupName) command -- @optional param delay, integer, delay to apply before replacing the mission, useful when teleporting, recommended 1s for such a scenario (which is the default value) -- @optional param immortal, boolean, sets the group which is seeing it's mission replaced to immortal and invisible ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMove.replaceMission(unitGroup, missionData, delay, immortal) local delay = delay or 1 local actualReplaceMission = function(unitGroup, missionData, immortal) local freq = missionData.frequency or 243 --set frequency or guard channel local mod = missionData.modulation or 0 --set modulation or AM (=0) veaf.loggers.get(veafMove.Id):debug(string.format("Resetting %s mission", unitGroup:getName())) veaf.loggers.get(veafMove.Id):debug(string.format("replaceMissionData=%s", veaf.p(missionData))) --... for the escort, necessary to re assign the mission wether the tanker was teleported (== changed ID) or not because DCS local mission = { id = 'Mission', params = missionData } local controller = unitGroup:getController() controller:setTask(mission) if immortal then -- JTAC needs to be invisible and immortal veaf.loggers.get(veafMove.Id):trace("Group immortalized") local _setImmortal = { id = 'SetImmortal', params = { value = true } } -- invisible to AI, Shagrat local _setInvisible = { id = 'SetInvisible', params = { value = true } } Controller.setCommand(controller, _setImmortal) Controller.setCommand(controller, _setInvisible) end --have to set the frequency again as setTask seems to ignore missionData.frequency and switch the unit to 124AM local _setFrequency = { id = 'SetFrequency', params = { frequency = freq * 1000000, modulation = mod } } Controller.setCommand(controller, _setFrequency) end mist.scheduleFunction(actualReplaceMission, {unitGroup, missionData, immortal}, timer.getTime()+delay) end ------------------------------------------------------------------------------ -- veafMove.moveAfac -- @param point eventPos -- @param string groupName -- @param float speed in knots -- @param float alt in feet -- @param float hdg in degrees -- @param boolean immortal ------------------------------------------------------------------------------ function veafMove.moveAfac(eventPos, groupName, speed, alt, heading, immortal) if not speed then speed = 150 end if not alt then alt = 20000 end veaf.loggers.get(veafMove.Id):debug("veafMove.moveAfac(groupName = " .. groupName .. ", speed = " .. speed .. ", alt = " .. alt) veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.moveAfac: eventPos x=%.1f z=%.1f", eventPos.x, eventPos.z)) local distanceFromTeleport = 3000 --distance between the orbit point and the teleport point in meters local unitGroup = Group.getByName(groupName) if unitGroup == nil then veaf.loggers.get(veafMove.Id):info(groupName .. ' not found for move afac command') trigger.action.outText(groupName .. ' not found for move afac command' , 10) return false end local coalition = unitGroup:getCoalition() local afacData = veaf.getGroupData(groupName) local isDynamicallySpawned = false if not afacData then for number, dynAFACcallsign in pairs(veafSpawn.AFAC.callsigns[coalition]) do if groupName:find(dynAFACcallsign.name) then veaf.loggers.get(veafMove.Id):trace("AFAC is dynamically spawned") afacData = veafSpawn.AFAC.missionData[coalition][number] isDynamicallySpawned = true end end end veaf.loggers.get(veafMove.Id):trace("Found AFAC named " .. groupName .. " for move command") veaf.loggers.get(veafMove.Id):debug(string.format("AFAC mission data is : %s", veaf.p(afacData))) local route_afac = veaf.findInTable(afacData, "route") local points_afac = veaf.findInTable(route_afac, "points") if points_afac then veaf.loggers.get(veafMove.Id):trace("Found AFAC waypoints") local idxPoint1_afac = #points_afac-1 --second to last waypoint local idxPoint2_afac = #points_afac --last waypoint local point1_afac = points_afac[idxPoint1_afac] local point2_afac = points_afac[idxPoint2_afac] local FACflag = false local OrbitFlag = false -- if hdg is not set, compute heading between point1 and point2 local hdg = heading if hdg == nil then hdg = veaf.headingBetweenPoints(point1_afac, point2_afac) else hdg = heading * math.pi/180 end -- teleport position local teleportPosition = { ["x"] = eventPos.x - distanceFromTeleport*math.cos(hdg), --teleport 3km south of orbit point ["y"] = eventPos.z - distanceFromTeleport*math.sin(hdg), ["alt"] = alt * 0.3048 -- in meters } -- orbit position local fromPosition = { ["x"] = eventPos.x, ["y"] = eventPos.z } --check valid configuration of the AFAC if point1_afac and point2_afac then veaf.loggers.get(veafMove.Id):debug("AFAC has at least the two waypoints required") local tasks1_afac=point1_afac.task.params.tasks local tasks2_afac=point2_afac.task.params.tasks if tasks1_afac and tasks2_afac then for _, task in pairs(tasks1_afac) do if task.id == "FAC" then veaf.loggers.get(veafMove.Id):trace("FAC configuration valid on second to last WP") FACflag = true end end for _, task in pairs(tasks2_afac) do if task.id == "Orbit" then veaf.loggers.get(veafMove.Id):trace("AFAC Orbit configuration valid on last WP") OrbitFlag = true end end end end if FACflag == false or OrbitFlag == false then veaf.loggers.get(veafMove.Id):info(groupName .. ' has an invalid FAC/Orbit configuration') trigger.action.outText(groupName .. ' has an invalid FAC/Orbit configuration' , 10) return false end --edit the last two waypoints of the AFAC's flight plan with the new requested position,speed and alt info point1_afac.speed=speed point1_afac.alt=teleportPosition.alt point1_afac.x=teleportPosition.x + distanceFromTeleport*math.cos(hdg)/2 point1_afac.y=teleportPosition.y + distanceFromTeleport*math.sin(hdg)/2 point2_afac.speed=speed point2_afac.alt=teleportPosition.alt point2_afac.x=eventPos.x point2_afac.y=eventPos.z --teleport the group south of the requested location veaf.loggers.get(veafMove.Id):trace("AFAC ".. groupName .. " teleported") local vars = { groupName = groupName, point = teleportPosition, action = "teleport" } if isDynamicallySpawned then vars = { groupName = groupName, groupData = afacData, anyTerrain = true, point = teleportPosition, action = "teleport" } end local grp = mist.teleportToPoint(vars) unitGroup = Group.getByName(groupName) --refresh group class after respawn, not necessary but safer considering at least the groupId changes --necessary delay for the following code to not be ignored local delay=1 -- replace whole mission veafMove.replaceMission(unitGroup, afacData, delay, immortal) return true else return false end end -- prepare tanker units function veafMove.findAllTankers() local TankerTypeNames = {"KC130", "KC-135", "KC135MPRS", "KJ-2000", "IL-78M"} veaf.loggers.get(veafMove.Id):trace(string.format("findAllTankers()")) local result = {} local units = mist.DBs.unitsByName -- local copy for faster execution for name, unit in pairs(units) do veaf.loggers.get(veafMove.Id):trace(string.format("name=%s, unit.type=%s", veaf.p(name), veaf.p(unit.type))) --veaf.loggers.get(veafMove.Id):trace(string.format("unit=%s", veaf.p(unit))) --local unit = Unit.getByName(name) if unit then for _, tankerTypeName in pairs(TankerTypeNames) do if tankerTypeName:lower() == unit.type:lower() then table.insert(result, unit.groupName) end end end end return result end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Build a radio menu to move or teleport a tanker function veafMove.moveTankerToMe(parameters) local subParameters, unitName = veaf.safeUnpack(parameters) local tankerName, direction = veaf.safeUnpack(subParameters) veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.moveTankerToMe(tankerName=%s, unitName=%s, direction=%d)", tankerName, unitName, direction)) local unit = Unit.getByName(unitName) if unit then local unitType = unit:getDesc()["typeName"] veaf.loggers.get(veafMove.Id):trace(string.format("checking unit %s of type %s", tostring(unitName), tostring(unitType))) local tankerMissionParameters = veafMove.tankerMissionParameters[unitType] if not tankerMissionParameters then tankerMissionParameters = { speed = -1, alt = -1} -- -1 means to use the currently defined speed and altitude end veafMove.moveTanker(unit:getPosition().p, tankerName, tankerMissionParameters.speed, tankerMissionParameters.alt, direction, nil, true, false) veaf.outTextForUnit(unitName, string.format("%s - Moving to your position right away !", tankerName), 15) end end --- Build the initial radio menu function veafMove.buildRadioMenu() veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.buildRadioMenu()")) veafMove.rootPath = veafRadio.addSubMenu(veafMove.RadioMenuName) if not(veafRadio.skipHelpMenus) then veafRadio.addCommandToSubmenu("HELP", veafMove.rootPath, veafMove.help, nil, veafRadio.USAGE_ForGroup) end for _, tankerUnitName in pairs(veafMove.Tankers) do local tankerName = tankerUnitName if veafAssets then veaf.loggers.get(veafMove.Id):trace(string.format("searching for asset name %s", tankerUnitName)) local asset = veafAssets.get(tankerUnitName) if asset then tankerName = asset.description veaf.loggers.get(veafMove.Id):trace(string.format("found asset name : %s", tankerName)) end end -- Move tanker to me local menuName = string.format("%s - WEST", tankerName) local moveTankerPath = veafRadio.addSubMenu(menuName, veafMove.rootPath) veafRadio.addCommandToSubmenu(menuName, moveTankerPath, veafMove.moveTankerToMe, {tankerUnitName, 270}, veafRadio.USAGE_ForGroup) menuName = string.format("%s - EAST", tankerName) moveTankerPath = veafRadio.addSubMenu(menuName, veafMove.rootPath) veafRadio.addCommandToSubmenu(menuName, moveTankerPath, veafMove.moveTankerToMe, {tankerUnitName, 90}, veafRadio.USAGE_ForGroup) end end function veafMove.help(unitName) local text = 'Create a marker and type "_move , name " in the text\n' .. 'This will issue a move command to the specified group in the DCS world\n' .. 'Type "_move group, name [groupname]" to move the specified group to the marker point\n' .. ' add ", speed [speed]" to make the group move and at the specified speed (in knots)\n' .. 'Type "_move tanker, name [groupname]" to create a new tanker flight plan and move the specified tanker.\n' .. ' add ", speed [speed]" to make the tanker move and execute its refuel mission at the specified speed (in knots)\n' .. ' add ", alt [altitude]" to specify the refuel leg altitude (in feet)\n' .. 'Type "_move afac, name [groupname]" to create a new JTAC flight plan and move the specified afac drone.\n' .. ' add ", speed [speed]" to make the tanker move and execute its mission at the specified speed (in knots)\n' .. ' add ", alt [altitude]" to specify the altitude at which the drone will circle (in feet)' veaf.outTextForGroup(unitName, text, 30) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMove.initialize() if #veafMove.Tankers == 0 then -- find all existing Tankers veafMove.Tankers = veafMove.findAllTankers() end veafMove.buildRadioMenu() veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafMove.onEventMarkChange) end veaf.loggers.get(veafMove.Id):info(string.format("Loading version %s", veafMove.Version)) ------------------ END script veafMove.lua ------------------ ------------------ START script veafNamedPoints.lua ------------------ ------------------------------------------------------------------ -- VEAF name point command and functions for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Listen to marker change events and name the corresponding point, for future reference -- * Works with all current and future maps (Caucasus, NTTR, Normandy, PG, ...) -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafNamedPoints = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafNamedPoints.Id = "NAMED POINTS" --- Version. veafNamedPoints.Version = "1.16.1" -- trace level, specific to this module --veafNamedPoints.LogLevel = "trace" veaf.loggers.new(veafNamedPoints.Id, veafNamedPoints.LogLevel) --- Key phrase to look for in the mark text which triggers the command. veafNamedPoints.Keyphrase = "_name point" veafNamedPoints.RadioMenuName = "NAMED POINTS" veafNamedPoints.RemoteCommandParser = "([[a-zA-Z0-9]+)%s?([^%s]*)%s?(.*)" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafNamedPoints.namedPoints = {} veafNamedPoints.rootPath = nil veafNamedPoints.markid = 1270000 --- Initial Marker id. ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function executed when a mark has changed. This happens when text is entered or changed. function veafNamedPoints.onEventMarkChange(eventPos, event) if veafNamedPoints.executeCommand(eventPos, event) then -- Delete old mark. veaf.loggers.get(veafNamedPoints.Id):trace(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafNamedPoints.executeCommand(eventPos, event, bypassSecurity) -- Check if marker has a text and the veafNamedPoints.keyphrase keyphrase. if event.text ~= nil and event.text:lower():find(veafNamedPoints.Keyphrase) then -- Analyse the mark point text and extract the keywords. local options = veafNamedPoints.markTextAnalysis(event.text) if options then -- Check options commands if options.namepoint then -- create the mission veafNamedPoints.namePoint(eventPos, options.name, event.coalition) end return true else -- None of the keywords matched. return false end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Analyse the mark text and extract keywords. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Extract keywords from mark text. function veafNamedPoints.markTextAnalysis(text) -- Option parameters extracted from the mark text. local switch = {} switch.namepoint = false switch.name = "point" -- Check for correct keywords. local pos = text:lower():find(veafNamedPoints.Keyphrase) if pos then switch.namepoint = true else return nil end -- the point name should follow a space switch.name = text:sub(pos+string.len(veafNamedPoints.Keyphrase)+1) veaf.loggers.get(veafNamedPoints.Id):debug(string.format("Keyword name = %s", switch.name)) return switch end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Named points management ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Create the point in the named points database function veafNamedPoints.namePoint(targetSpot, name, coalition, silent) veaf.loggers.get(veafNamedPoints.Id):debug(string.format("namePoint(name = %s, coalition=%s)",name, coalition)) veaf.loggers.get(veafNamedPoints.Id):debug("targetSpot=" .. veaf.vecToString(targetSpot)) -- find an existing point with the same name local existingPoint = veafNamedPoints.getPoint(name) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("existingPoint=%s", veaf.p(existingPoint))) if existingPoint and existingPoint.markerId then -- delete the existing point trigger.action.removeMark(existingPoint.markerId) end local point = { x = targetSpot.x, y = targetSpot.y, z = targetSpot.z} point.hidden = false veafNamedPoints.addPoint(name, point) local message = nil if not silent then message = "VEAF - Point named "..name.." added for own coalition." end veafNamedPoints.markid = veafNamedPoints.markid + 1 point.markerId = veafNamedPoints.markid trigger.action.markToCoalition(veafNamedPoints.markid, "VEAF - Point named "..name, point, coalition, true, message) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("created point %s", veaf.p(point))) end function veafNamedPoints.addPoint(name, point) if not point.y then point.y = 0 end veaf.loggers.get(veafNamedPoints.Id):trace(string.format("addPoint: {name=\"%s\",point={x=%d,y=0,z=%d}}", name, point.x, point.z)) point.name = name:upper() veafNamedPoints.namedPoints[name:upper()] = point return point end function veafNamedPoints.delPoint(name) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("delPoint(name = %s)",name)) table.remove(veafNamedPoints.namedPoints, name:upper()) end function veafNamedPoints.getPoint(name) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("getPoint(name = %s)",name or "")) if name then return veafNamedPoints.namedPoints[name:upper()] else return nil end end function veafNamedPoints.getPointBearing(parameters) local name, unitName = veaf.safeUnpack(parameters) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("getPointBearing(%s)",name)) local point = veafNamedPoints.getPoint(name) local unit = veafRadio.getHumanUnitOrWingman(unitName) if point and unit then local angle, distance, distanceInKm, distanceInNm = veaf.getBearingAndRangeFromTo(unit:getPosition().p, point) if distanceInNm > 2 then return "at " .. angle .. "° for " .. distanceInNm .. " nm" end end return nil end function veafNamedPoints.getNearestPoint(unitName) veaf.loggers.get(veafNamedPoints.Id):debug("veafNamedPoints.getNearestPoint(unitName = %s)",veaf.p(unitName)) local closestPoint = nil local minDistance = 99999999 local unit = veafRadio.getHumanUnitOrWingman(unitName) if unit then for name, point in pairs(veafNamedPoints.namedPoints) do local distanceFromPlayer = ((point.x - unit:getPosition().p.x)^2 + (point.z - unit:getPosition().p.z)^2)^0.5 veaf.loggers.get(veafNamedPoints.Id):trace(string.format("name=%s, distanceFromPlayer=%d",name, distanceFromPlayer)) if distanceFromPlayer < minDistance then minDistance = distanceFromPlayer closestPoint = point end end end veaf.loggers.get(veafNamedPoints.Id):trace("closestPoint=%s",veaf.p(closestPoint)) return closestPoint end function veafNamedPoints.pointFromString(coordinatesString) veaf.loggers.get(veafNamedPoints.Id):debug(string.format("pointFromString(coordinatesString = %s)",veaf.p(coordinatesString))) local _result = nil local _lat, _lon = veaf.computeLLFromString(coordinatesString) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("_lat=%s",veaf.p(_lat))) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("_lon=%s",veaf.p(_lon))) if _lat and _lon then _result = veafNamedPoints.pointFromLL(_lat, _lon) end return _result end function veafNamedPoints.pointFromLL(lat, long) veaf.loggers.get(veafNamedPoints.Id):debug(string.format("pointFromLL(lat = %s, long = %s)",veaf.p(lat), veaf.p(long))) return coord.LLtoLO(lat, long) end function veafNamedPoints.addDataToPoint(point, data) if point then if data then for key, value in pairs(data) do point[key] = value end end return point end end function veafNamedPoints.listAllPoints(unitName) veaf.loggers.get(veafNamedPoints.Id):debug(string.format("listAllPoints(unitName = %s)",tostring(unitName))) local message = "" local names = {} for name, point in pairs(veafNamedPoints.namedPoints) do if not point.hidden then table.insert(names, name) end end table.sort(names) for _, name in pairs(names) do local point = veafNamedPoints.namedPoints[name] local lat, lon = coord.LOtoLL(point) local llString = mist.tostringLL(lat, lon, 3) llString = llString:sub(0,2) .. '°' .. llString:sub(4) local mgrs = coord.LLtoMGRS(lat, lon) local mgrsString = mist.tostringMGRS(mgrs, 5) message = message .. name .. " => " .. llString .. " / " .. mgrsString .. "\n" end -- send message only for the unit veaf.outTextForUnit(unitName, message, 30) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Build the initial radio menu function veafNamedPoints.buildRadioMenu() veaf.loggers.get(veafNamedPoints.Id):debug("buildRadioMenu()") veafNamedPoints.rootPath = veafRadio.addSubMenu(veafNamedPoints.RadioMenuName) veafRadio.addCommandToSubmenu("List all points", veafNamedPoints.rootPath, veafNamedPoints.listAllPoints, nil, veafRadio.USAGE_ForGroup) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- execute command from the remote interface function veafNamedPoints.executeCommandFromRemote(parameters) veaf.loggers.get(veafNamedPoints.Id):debug(string.format("veafNamedPoints.executeCommandFromRemote()")) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("_pilot= %s", veaf.p(_pilot))) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName))) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("_unitName= %s", veaf.p(_unitName))) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("_command= %s", veaf.p(_command))) if not _pilot or not _command then return false end veaf.outTextForUnit(_unitName, "no remote command for veafNamedPoints; for atc and weather try -weather", 30) --[[ keep this for later if we need a remote access for the veafNamedPoints script if _command then -- parse the command local _action, _pointName, _parameters = _command:match(veafNamedPoints.RemoteCommandParser) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("_action=%s",veaf.p(_action))) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("_pointName=%s",veaf.p(_pointName))) veaf.loggers.get(veafNamedPoints.Id):trace(string.format("_parameters=%s",veaf.p(_parameters))) if _action and _action:lower() == "weather" then veaf.loggers.get(veafNamedPoints.Id):info(string.format("[%s] is requesting weather at his position",veaf.p(_pilotName))) veafNamedPoints.getWeatherAtClosestPoint(_unitName, true) return true elseif _action and _action:lower() == "atc" then veaf.loggers.get(veafNamedPoints.Id):info(string.format("[%s] is requesting atc at his position",veaf.p(_pilotName))) veafNamedPoints.getAtcAndWeatherAtClosestPoint(_unitName, true) return true end end return false ]] end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafNamedPoints.initialize(customPoints) veaf.loggers.get(veafNamedPoints.Id):info("Initialize veafNamedPoints") veafNamedPoints.namedPoints = {} veafNamedPoints.addCities() veafNamedPoints.addAirbases() veafNamedPoints.addCustomPoints(customPoints) veafNamedPoints.buildRadioMenu() veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafNamedPoints.onEventMarkChange) end function veafNamedPoints.addCities() local theatre = env.mission.theatre veaf.loggers.get(veafNamedPoints.Id):info("Initialize veafNamedPoints cities for theatre %s", theatre) local theatreCities if theatre == veaf.theatreName.Syria then theatreCities = veafNamedPoints._citiesSyria elseif theatre == veaf.theatreName.Caucasus then theatreCities = veafNamedPoints._citiesCaucasus elseif theatre == veaf.theatreName.PersianGulf then theatreCities = veafNamedPoints._citiesPersianGulf elseif theatre == veaf.theatreName.TheChannel then theatreCities = veafNamedPoints._citiesTheChannel elseif theatre == veaf.theatreName.MarianaIslands then theatreCities = veafNamedPoints._citiesMarianasIslands elseif theatre == veaf.theatreName.Falklands then theatreCities = veafNamedPoints._citiesFalklands else veaf.loggers.get(veaf.Id):warn(string.format("no cities in veafNamedPoints for theatre %s ", theatre)) theatreCities = {} end veafNamedPoints.addCitiesFromList(theatreCities) end function veafNamedPoints.addCitiesFromList(cities) for name, data in pairs(cities) do veaf.loggers.get(veafNamedPoints.Id):trace(string.format("processing city name=[%s]",name or "")) local point = coord.LLtoLO(data.latitude, data.longitude) --veaf.loggers.get(veafNamedPoints.Id):trace(string.format("point=[%s]",veaf.p(point))) point.hidden = true local name = data.display_name --veaf.loggers.get(veafNamedPoints.Id):trace(string.format("name=[%s]",name or "")) veafNamedPoints.addPoint(name, point) -- add a clean version of the city name local cleanedUpCityName = name:gsub("[^a-zA-Z]","") if not veafNamedPoints.namedPoints[cleanedUpCityName:upper()] then veafNamedPoints.addPoint(cleanedUpCityName, point) end --veafNamedPoints.markid = veafNamedPoints.markid + 1 --trigger.action.markToAll(veafNamedPoints.markid, "VEAF - Point named "..name, point, true) end end function veafNamedPoints.addAirbases() veafAirbases.initialize() for _, veafAirbase in pairs(veafAirbases.Airbases) do if (veafAirbase.Category == Airbase.Category.AIRDROME) then veaf.loggers.get(veafNamedPoints.Id):trace("processing airbase name=[%s]", veafAirbase.DisplayName) local vec3 = veafAirbase.DcsAirbase:getPoint() local runways = {} for i, veafRunway in ipairs(veafAirbase.Runways) do for __, veafRunwayEnd in ipairs(veafRunway) do --veaf.loggers.get(veafNamedPoints.Id):trace(veaf.p(veafRunwayEnd)) table.insert(runways, {name = string.format("%02d", veafRunwayEnd.Number), hdg = veafRunwayEnd.Heading }) end end local namedPoint = { x = vec3.x, y = 0, z = vec3.z, runways = runways} --{name="AIRBASE Batumi", point={x=-356437,y=0,z=618211, atc=true, tower="V131, U260", tacan="16X BTM", runways={{name="13", hdg=125, ils="110.30"}, {name="31", hdg=305}}}}, namedPoint.hidden = true veafNamedPoints.addPoint(string.format("AIRBASE %s", veafAirbase.DisplayName), namedPoint) end end return nil end function veafNamedPoints.addCustomPoints(customPoints) veaf.loggers.get(veafNamedPoints.Id):debug("addCustomPoints()") if customPoints then for _, defaultPoint in pairs(customPoints) do veafNamedPoints.addPoint(defaultPoint.name, defaultPoint.point) end end end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- Cities tables --- these tables below comes from the game, and contain duplicate indexes --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- -- backward compatibility for old mission created with pre-2023 VMC function veafNamedPoints.addAllSyriaCities() end function veafNamedPoints.addAllCaucasusCities() end function veafNamedPoints.addAllTheChannelCities() end function veafNamedPoints.addAllMarianasIslandsCities() end function veafNamedPoints.addAllPersianGulfCities() end -- ---@diagnostic disable: duplicate-index veafNamedPoints._citiesCaucasus = { ["AMBROLAURI"] = { latitude = 42.530195, longitude = 43.150121, display_name = "AMBROLAURI"}, ["KUTAISI"] = { latitude = 42.267086, longitude = 42.696849, display_name = "KUTAISI"}, ["BATUMI"] = { latitude = 41.654059, longitude = 41.655372, display_name = "BATUMI"}, ["POTI"] = { latitude = 42.157804, longitude = 41.677693, display_name = "POTI"}, ["ZUGDIDI"] = { latitude = 42.516379, longitude = 41.879016, display_name = "ZUGDIDI"}, ["MAYKOP"] = { latitude = 44.607053, longitude = 40.096197, display_name = "MAYKOP"}, ["KRASNODAR"] = { latitude = 45.053964, longitude = 39.000128, display_name = "KRASNODAR"}, ["NOVOROSSIYSK"] = { latitude = 44.719335, longitude = 37.751757, display_name = "NOVOROSSIYSK"}, ["KISLOVODSK"] = { latitude = 43.913866, longitude = 42.723910, display_name = "KISLOVODSK"}, ["SUKHUMI"] = { latitude = 43.010963, longitude = 40.999400, display_name = "SUKHUMI"}, ["SOCHI"] = { latitude = 43.604619, longitude = 39.721483, display_name = "SOCHI"}, ["NAL'CHIK"] = { latitude = 43.485073, longitude = 43.623441, display_name = "NAL'CHIK"}, ["PYATIGORSK"] = { latitude = 44.052891, longitude = 43.048264, display_name = "PYATIGORSK"}, ["MINERAL'NYE VODY"] = { latitude = 44.201454, longitude = 43.137392, display_name = "MINERAL'NYE VODY"}, ["GEORGIEVSK"] = { latitude = 44.148896, longitude = 43.458776, display_name = "GEORGIEVSK"}, ["CHERKESSK"] = { latitude = 44.226950, longitude = 42.060347, display_name = "CHERKESSK"}, ["LANCHHUTI"] = { latitude = 42.089007, longitude = 42.027661, display_name = "LANCHHUTI"}, ["SAMTREDIA"] = { latitude = 42.174739, longitude = 42.336138, display_name = "SAMTREDIA"}, ["ABASHA"] = { latitude = 42.221963, longitude = 42.211652, display_name = "ABASHA"}, ["SENAKI"] = { latitude = 42.285411, longitude = 42.045822, display_name = "SENAKI"}, ["HONI"] = { latitude = 42.326943, longitude = 42.412159, display_name = "HONI"}, ["MARTVILI"] = { latitude = 42.400702, longitude = 42.368318, display_name = "MARTVILI"}, ["TSHALTUBO"] = { latitude = 42.323088, longitude = 42.614660, display_name = "TSHALTUBO"}, ["TKIBULI"] = { latitude = 42.352385, longitude = 42.997191, display_name = "TKIBULI"}, ["ZESTAFONI"] = { latitude = 42.111455, longitude = 43.037536, display_name = "ZESTAFONI"}, ["CHIATURA"] = { latitude = 42.293616, longitude = 43.270537, display_name = "CHIATURA"}, ["SACHHERE"] = { latitude = 42.342501, longitude = 43.404049, display_name = "SACHHERE"}, ["OZURGETI"] = { latitude = 41.936007, longitude = 42.018441, display_name = "OZURGETI"}, ["KOBULETI"] = { latitude = 41.808106, longitude = 41.780942, display_name = "KOBULETI"}, ["ONI"] = { latitude = 42.591213, longitude = 43.449897, display_name = "ONI"}, ["DZHVARI"] = { latitude = 42.703382, longitude = 42.042828, display_name = "DZHVARI"}, ["GALI"] = { latitude = 42.624895, longitude = 41.727731, display_name = "GALI"}, ["OCHAMCHIRA"] = { latitude = 42.722153, longitude = 41.474646, display_name = "OCHAMCHIRA"}, ["UST'-DZHEGUTA"] = { latitude = 44.080182, longitude = 41.969081, display_name = "UST'-DZHEGUTA"}, ["APSHERONSK"] = { latitude = 44.459654, longitude = 39.729136, display_name = "APSHERONSK"}, ["TUAPSE"] = { latitude = 44.114132, longitude = 39.068294, display_name = "TUAPSE"}, ["GELENDZHIK"] = { latitude = 44.578432, longitude = 38.018660, display_name = "GELENDZHIK"}, ["PASHKOVSKIY"] = { latitude = 45.029117, longitude = 39.094463, display_name = "PASHKOVSKIY"}, ["UST'-LABINSK"] = { latitude = 45.221626, longitude = 39.683478, display_name = "UST'-LABINSK"}, ["BELORECHENSK"] = { latitude = 44.771687, longitude = 39.875183, display_name = "BELORECHENSK"}, ["ABINSK"] = { latitude = 44.870561, longitude = 38.156475, display_name = "ABINSK"}, ["KRYMSK"] = { latitude = 44.927837, longitude = 38.011490, display_name = "KRYMSK"}, ["SLAVYANSK-NA-KUBANI"] = { latitude = 45.253951, longitude = 38.123431, display_name = "SLAVYANSK-NA-KUBANI"}, ["ANAPA"] = { latitude = 44.932503, longitude = 37.298006, display_name = "ANAPA"}, ["TEMRYUK"] = { latitude = 45.261076, longitude = 37.432072, display_name = "TEMRYUK"}, ["TKVARCHELI"] = { latitude = 42.853781, longitude = 41.674307, display_name = "TKVARCHELI"}, ["ESSENTUKI"] = { latitude = 44.043909, longitude = 42.860779, display_name = "ESSENTUKI"}, ["ADLER"] = { latitude = 43.453776, longitude = 39.915595, display_name = "ADLER"}, ["HOSTA"] = { latitude = 43.515628, longitude = 39.864624, display_name = "HOSTA"}, ["LAZAREVSKOE"] = { latitude = 43.917274, longitude = 39.326891, display_name = "LAZAREVSKOE"}, ["LERMONTOV"] = { latitude = 44.108783, longitude = 42.974445, display_name = "LERMONTOV"}, ["TYRNYAUZ"] = { latitude = 43.391341, longitude = 42.921969, display_name = "TYRNYAUZ"}, ["NOVOPAVLOVSK"] = { latitude = 43.962533, longitude = 43.641826, display_name = "NOVOPAVLOVSK"}, ["BAKSAN"] = { latitude = 43.686840, longitude = 43.544814, display_name = "BAKSAN"}, ["NARTKALA"] = { latitude = 43.554316, longitude = 43.853943, display_name = "NARTKALA"}, ["ZHELEZNOVODSK"] = { latitude = 44.141803, longitude = 43.023059, display_name = "ZHELEZNOVODSK"}, ["Helvachauri"] = { latitude = 41.596451, longitude = 41.668302, display_name = "Helvachauri"}, ["Mahindzhauri"] = { latitude = 41.673079, longitude = 41.699531, display_name = "Mahindzhauri"}, ["Kur.Bahmaro"] = { latitude = 41.848456, longitude = 42.327860, display_name = "Kur.Bahmaro"}, ["Kur.Sairme"] = { latitude = 41.907942, longitude = 42.743349, display_name = "Kur.Sairme"}, ["Chakva"] = { latitude = 41.727030, longitude = 41.734648, display_name = "Chakva"}, ["Laituri"] = { latitude = 41.918270, longitude = 41.900378, display_name = "Laituri"}, ["Kvedo-Nasakirali"] = { latitude = 41.977199, longitude = 42.061216, display_name = "Kvedo-Nasakirali"}, ["Kulashi"] = { latitude = 42.210505, longitude = 42.349080, display_name = "Kulashi"}, ["Gagma-Pirveli-Horga"] = { latitude = 42.267520, longitude = 41.859143, display_name = "Gagma-Pirveli-Horga"}, ["Gamogma-Pirveli-Horga"] = { latitude = 42.288279, longitude = 41.845307, display_name = "Gamogma-Pirveli-Horga"}, ["Ordzhonikidze"] = { latitude = 42.008849, longitude = 43.173219, display_name = "Ordzhonikidze"}, ["Terzhola"] = { latitude = 42.195788, longitude = 42.983178, display_name = "Terzhola"}, ["Haristvala"] = { latitude = 42.415940, longitude = 43.041115, display_name = "Haristvala"}, ["Zeda-Sairme"] = { latitude = 42.564224, longitude = 42.882795, display_name = "Zeda-Sairme"}, ["kur.Skuri"] = { latitude = 42.697552, longitude = 42.161497, display_name = "kur.Skuri"}, ["Tsalendzhiha"] = { latitude = 42.619409, longitude = 42.070900, display_name = "Tsalendzhiha"}, ["Muzhava"] = { latitude = 42.713559, longitude = 41.992057, display_name = "Muzhava"}, ["Ingurges"] = { latitude = 42.672937, longitude = 41.853299, display_name = "Ingurges"}, ["Severo-Vostochnye Sady"] = { latitude = 44.635280, longitude = 40.131508, display_name = "Severo-Vostochnye Sady"}, ["Psebay"] = { latitude = 44.138091, longitude = 40.803699, display_name = "Psebay"}, ["Kamennomostskiy"] = { latitude = 44.302330, longitude = 40.184071, display_name = "Kamennomostskiy"}, ["Tul'skiy"] = { latitude = 44.513927, longitude = 40.170579, display_name = "Tul'skiy"}, ["Magri"] = { latitude = 44.022796, longitude = 39.163094, display_name = "Magri"}, ["Vishnevka"] = { latitude = 44.010679, longitude = 39.186179, display_name = "Vishnevka"}, ["Goryachiy Klyuch"] = { latitude = 44.631585, longitude = 39.125779, display_name = "Goryachiy Klyuch"}, ["Hadyzhensk"] = { latitude = 44.427144, longitude = 39.530325, display_name = "Hadyzhensk"}, ["Grozneft'"] = { latitude = 44.096137, longitude = 39.100269, display_name = "Grozneft'"}, ["Novomihaylovskiy"] = { latitude = 44.258430, longitude = 38.854150, display_name = "Novomihaylovskiy"}, ["Dzhubga"] = { latitude = 44.325968, longitude = 38.703843, display_name = "Dzhubga"}, ["Arhipo-Osipovka"] = { latitude = 44.376981, longitude = 38.530670, display_name = "Arhipo-Osipovka"}, ["Pshada"] = { latitude = 44.474606, longitude = 38.401898, display_name = "Pshada"}, ["Divnomorskoe"] = { latitude = 44.503462, longitude = 38.132761, display_name = "Divnomorskoe"}, ["Kabardinka"] = { latitude = 44.657320, longitude = 37.934694, display_name = "Kabardinka"}, ["Giaginskaya"] = { latitude = 44.872978, longitude = 40.056178, display_name = "Giaginskaya"}, ["Vasyurinskaya"] = { latitude = 45.117847, longitude = 39.420083, display_name = "Vasyurinskaya"}, ["Adygeysk"] = { latitude = 44.887801, longitude = 39.187143, display_name = "Adygeysk"}, ["Tlyustenhabl'"] = { latitude = 44.980442, longitude = 39.092968, display_name = "Tlyustenhabl'"}, ["Kalinino"] = { latitude = 45.097761, longitude = 39.016695, display_name = "Kalinino"}, ["Afipskiy"] = { latitude = 44.904156, longitude = 38.844197, display_name = "Afipskiy"}, ["Il'skiy"] = { latitude = 44.843810, longitude = 38.563777, display_name = "Il'skiy"}, ["Chernomorskiy"] = { latitude = 44.851048, longitude = 38.491750, display_name = "Chernomorskiy"}, ["Holmskiy"] = { latitude = 44.844059, longitude = 38.390536, display_name = "Holmskiy"}, ["Ahtyrskiy"] = { latitude = 44.848464, longitude = 38.305640, display_name = "Ahtyrskiy"}, ["Enem"] = { latitude = 44.926601, longitude = 38.903816, display_name = "Enem"}, ["Yablonovskiy"] = { latitude = 44.986937, longitude = 38.944217, display_name = "Yablonovskiy"}, ["Troitskiy"] = { latitude = 45.146496, longitude = 38.133592, display_name = "Troitskiy"}, ["Gayduk"] = { latitude = 44.787974, longitude = 37.699085, display_name = "Gayduk"}, ["Nizhnebakanskiy "] = { latitude = 44.868592, longitude = 37.861945, display_name = "Nizhnebakanskiy "}, ["Verhnebakanskiy "] = { latitude = 44.843629, longitude = 37.660287, display_name = "Verhnebakanskiy "}, ["Abrau-Dyurso"] = { latitude = 44.701124, longitude = 37.599438, display_name = "Abrau-Dyurso"}, ["Achigvara"] = { latitude = 42.680590, longitude = 41.628738, display_name = "Achigvara"}, ["Dzhukmur"] = { latitude = 42.746085, longitude = 41.450590, display_name = "Dzhukmur"}, ["Okumi"] = { latitude = 42.724566, longitude = 41.754616, display_name = "Okumi"}, ["Chkhortoli"] = { latitude = 42.761492, longitude = 41.733044, display_name = "Chkhortoli"}, ["Beshaluba"] = { latitude = 42.757691, longitude = 41.515278, display_name = "Beshaluba"}, ["Merkula"] = { latitude = 42.765603, longitude = 41.478017, display_name = "Merkula"}, ["Aradu"] = { latitude = 42.779854, longitude = 41.463354, display_name = "Aradu"}, ["Tsagera"] = { latitude = 42.779645, longitude = 41.422951, display_name = "Tsagera"}, ["Labra"] = { latitude = 42.812025, longitude = 41.394992, display_name = "Labra"}, ["Varcha"] = { latitude = 42.841492, longitude = 41.138267, display_name = "Varcha"}, ["Babushara"] = { latitude = 42.853206, longitude = 41.116261, display_name = "Babushara"}, ["Adzyubzha"] = { latitude = 42.838793, longitude = 41.191593, display_name = "Adzyubzha"}, ["Arakich"] = { latitude = 42.845023, longitude = 41.248912, display_name = "Arakich"}, ["Estonka"] = { latitude = 42.887601, longitude = 41.199704, display_name = "Estonka"}, ["Nizh.- Pshap"] = { latitude = 42.888750, longitude = 41.125540, display_name = "Nizh.- Pshap"}, ["Verh.- Pshap"] = { latitude = 42.891730, longitude = 41.155013, display_name = "Verh.- Pshap"}, ["Shaumyanovka"] = { latitude = 42.909981, longitude = 41.190234, display_name = "Shaumyanovka"}, ["Bagazhiashta"] = { latitude = 42.918151, longitude = 41.132147, display_name = "Bagazhiashta"}, ["Gul'ripsh"] = { latitude = 42.926240, longitude = 41.101427, display_name = "Gul'ripsh"}, ["Lentehi"] = { latitude = 42.790946, longitude = 42.724648, display_name = "Lentehi"}, ["Mestia"] = { latitude = 43.049600, longitude = 42.729598, display_name = "Mestia"}, ["Pervomayskoe"] = { latitude = 43.939875, longitude = 42.487590, display_name = "Pervomayskoe"}, ["Uchkeken"] = { latitude = 43.943106, longitude = 42.514172, display_name = "Uchkeken"}, ["Tereze"] = { latitude = 43.934122, longitude = 42.446490, display_name = "Tereze"}, ["Bambora"] = { latitude = 43.101065, longitude = 40.593551, display_name = "Bambora"}, ["Gudauta"] = { latitude = 43.106705, longitude = 40.635279, display_name = "Gudauta"}, ["Novyy Afon"] = { latitude = 43.090391, longitude = 40.809345, display_name = "Novyy Afon"}, ["Gagra"] = { latitude = 43.296491, longitude = 40.249993, display_name = "Gagra"}, ["Bzyb'"] = { latitude = 43.229864, longitude = 40.360523, display_name = "Bzyb'"}, ["Myussera"] = { latitude = 43.155608, longitude = 40.454065, display_name = "Myussera"}, ["Pitsunda"] = { latitude = 43.163846, longitude = 40.338462, display_name = "Pitsunda"}, ["Teberda"] = { latitude = 43.452155, longitude = 41.739692, display_name = "Teberda"}, ["Karachaevsk"] = { latitude = 43.770235, longitude = 41.898619, display_name = "Karachaevsk"}, ["Ordzhonikidzevskiy"] = { latitude = 43.844148, longitude = 41.892512, display_name = "Ordzhonikidzevskiy"}, ["Gantiadi"] = { latitude = 43.393301, longitude = 40.088996, display_name = "Gantiadi"}, ["Krasnaya Polyana"] = { latitude = 43.682548, longitude = 40.204334, display_name = "Krasnaya Polyana"}, ["Kurdzhinovo"] = { latitude = 44.003499, longitude = 40.946645, display_name = "Kurdzhinovo"}, ["Nov.Matsesta"] = { latitude = 43.560079, longitude = 39.801623, display_name = "Nov.Matsesta"}, ["Star.Matsesta"] = { latitude = 43.582022, longitude = 39.802334, display_name = "Star.Matsesta"}, ["Dagomys"] = { latitude = 43.665053, longitude = 39.658538, display_name = "Dagomys"}, ["UDARNYY"] = { latitude = 44.351550, longitude = 42.502748, display_name = "UDARNYY"}, ["SVOBODY"] = { latitude = 44.025747, longitude = 43.044756, display_name = "SVOBODY"}, ["GORYACHEVODSKIY"] = { latitude = 44.023610, longitude = 43.098525, display_name = "GORYACHEVODSKIY"}, ["INOZEMTSEVO"] = { latitude = 44.099998, longitude = 43.088585, display_name = "INOZEMTSEVO"}, ["ANDZHIEVSKIY"] = { latitude = 44.239664, longitude = 43.084935, display_name = "ANDZHIEVSKIY"}, ["Soldato-Aleksandrovskoe"] = { latitude = 44.266128, longitude = 43.758362, display_name = "Soldato-Aleksandrovskoe"}, ["Aleksandriyskaya"] = { latitude = 44.226368, longitude = 43.341033, display_name = "Aleksandriyskaya"}, ["Podgornaya"] = { latitude = 44.201130, longitude = 43.427100, display_name = "Podgornaya"}, ["ZALUKOKOAZHE"] = { latitude = 43.902365, longitude = 43.218663, display_name = "ZALUKOKOAZHE"}, ["CHEGEM PERVYY"] = { latitude = 43.569862, longitude = 43.581955, display_name = "CHEGEM PERVYY"}, ["KENZHE"] = { latitude = 43.499694, longitude = 43.554187, display_name = "KENZHE"}, ["HASAN'YA"] = { latitude = 43.438242, longitude = 43.578239, display_name = "HASAN'YA"}, ["BELAYA RECHKA"] = { latitude = 43.434090, longitude = 43.509509, display_name = "BELAYA RECHKA"}, ["KASHHATAU"] = { latitude = 43.318498, longitude = 43.607567, display_name = "KASHHATAU"}, ["ZHemtala"] = { latitude = 43.285507, longitude = 43.651954, display_name = "ZHemtala"}, ["Zaragizh"] = { latitude = 43.331056, longitude = 43.707394, display_name = "Zaragizh"}, ["Yanikoy"] = { latitude = 43.544208, longitude = 43.507782, display_name = "Yanikoy"}, ["ZHanhoteko"] = { latitude = 43.560102, longitude = 43.207769, display_name = "ZHanhoteko"}, ["Zayukovo"] = { latitude = 43.620891, longitude = 43.316939, display_name = "Zayukovo"}, ["Yantarnoe"] = { latitude = 43.762016, longitude = 43.881024, display_name = "Yantarnoe"}, ["Zalukodes"] = { latitude = 43.836692, longitude = 43.154045, display_name = "Zalukodes"}, ["Zol'skoe"] = { latitude = 43.842530, longitude = 43.202659, display_name = "Zol'skoe"}, ["Zol'skaya"] = { latitude = 43.905414, longitude = 43.299699, display_name = "Zol'skaya"}, ["Zarechnoe"] = { latitude = 43.937403, longitude = 43.856933, display_name = "Zarechnoe"}, ["Yutsa"] = { latitude = 43.967055, longitude = 43.011684, display_name = "Yutsa"}, ["Zakavkazskiy Partizan"] = { latitude = 44.116442, longitude = 43.838405, display_name = "Zakavkazskiy Partizan"}, ["Zmeyka"] = { latitude = 44.141831, longitude = 43.116065, display_name = "Zmeyka"}, ["Zagorskiy"] = { latitude = 44.258175, longitude = 43.124187, display_name = "Zagorskiy"}, ["ZHeleznodorozhnyy"] = { latitude = 44.283840, longitude = 43.730236, display_name = "ZHeleznodorozhnyy"}, ["Yasnaya Polyana"] = { latitude = 44.026131, longitude = 42.751359, display_name = "Yasnaya Polyana"}, ["Zolotushka"] = { latitude = 44.048106, longitude = 42.968682, display_name = "Zolotushka"}, ["Zheleznovodskiy"] = { latitude = 44.155908, longitude = 42.989749, display_name = "Zheleznovodskiy"}, ["Vodorazdel'nyy"] = { latitude = 44.253379, longitude = 42.343575, display_name = "Vodorazdel'nyy"}, ["Vorovskolesskaya"] = { latitude = 44.379548, longitude = 42.404731, display_name = "Vorovskolesskaya"}, ["Volkonka"] = { latitude = 43.869753, longitude = 39.394933, display_name = "Volkonka"}, ["Volkovka"] = { latitude = 43.697214, longitude = 39.665990, display_name = "Volkovka"}, ["Vorontsovka"] = { latitude = 43.620869, longitude = 39.918245, display_name = "Vorontsovka"}, ["Verhne-Veseloe"] = { latitude = 43.426208, longitude = 39.979875, display_name = "Verhne-Veseloe"}, ["Veseloe"] = { latitude = 43.412545, longitude = 40.002760, display_name = "Veseloe"}, ["Zelenchukskaya"] = { latitude = 43.858968, longitude = 41.582415, display_name = "Zelenchukskaya"}, ["Verhnyaya-Mtsara"] = { latitude = 43.169050, longitude = 40.766431, display_name = "Verhnyaya-Mtsara"}, ["Verhnyaya Eshera"] = { latitude = 43.075860, longitude = 40.892093, display_name = "Verhnyaya Eshera"}, ["Znamenka"] = { latitude = 44.146904, longitude = 42.058306, display_name = "Znamenka"}, ["Vinsady"] = { latitude = 44.080854, longitude = 42.959745, display_name = "Vinsady"}, ["Zemo-Machara"] = { latitude = 43.007251, longitude = 41.185583, display_name = "Zemo-Machara"}, ["Zemo-Azhara"] = { latitude = 43.111753, longitude = 41.749381, display_name = "Zemo-Azhara"}, ["Vladimirovka"] = { latitude = 42.893409, longitude = 41.233317, display_name = "Vladimirovka"}, ["Zaporozhskaya"] = { latitude = 45.380489, longitude = 36.863126, display_name = "Zaporozhskaya"}, ["Volna Revolyutsii"] = { latitude = 45.335780, longitude = 36.945070, display_name = "Volna Revolyutsii"}, ["Vinogradnyy"] = { latitude = 45.194969, longitude = 36.898478, display_name = "Vinogradnyy"}, ["Veselovka"] = { latitude = 45.130495, longitude = 36.900949, display_name = "Veselovka"}, ["Vyshesteblievskaya"] = { latitude = 45.197182, longitude = 36.997904, display_name = "Vyshesteblievskaya"}, ["Vestnik"] = { latitude = 45.102251, longitude = 37.452285, display_name = "Vestnik"}, ["Yurovka"] = { latitude = 45.116079, longitude = 37.412790, display_name = "Yurovka"}, ["Vinogradnyy"] = { latitude = 45.058888, longitude = 37.327405, display_name = "Vinogradnyy"}, ["Vityazevo"] = { latitude = 44.998498, longitude = 37.272707, display_name = "Vityazevo"}, ["Voskresenskiy"] = { latitude = 44.967396, longitude = 37.326592, display_name = "Voskresenskiy"}, ["Vinogradnyy"] = { latitude = 44.980235, longitude = 37.912105, display_name = "Vinogradnyy"}, ["Vladimirovka"] = { latitude = 44.792068, longitude = 37.675603, display_name = "Vladimirovka"}, ["Vorontsovskaya"] = { latitude = 45.221638, longitude = 38.736370, display_name = "Vorontsovskaya"}, ["Yuzhnyy"] = { latitude = 45.033052, longitude = 38.079312, display_name = "Yuzhnyy"}, ["ZHeleznyy"] = { latitude = 45.299309, longitude = 39.553831, display_name = "ZHeleznyy"}, ["Voronezhskaya"] = { latitude = 45.212918, longitude = 39.561800, display_name = "Voronezhskaya"}, ["Zarozhdenie"] = { latitude = 45.159335, longitude = 39.231552, display_name = "Zarozhdenie"}, ["Znamenskiy"] = { latitude = 45.060774, longitude = 39.142515, display_name = "Znamenskiy"}, ["Vysotnyy"] = { latitude = 44.956537, longitude = 39.680462, display_name = "Vysotnyy"}, ["Vochepshiy"] = { latitude = 44.876265, longitude = 39.284439, display_name = "Vochepshiy"}, ["Zarechnyy"] = { latitude = 44.757202, longitude = 39.826973, display_name = "Zarechnyy"}, ["Yuzhnyy"] = { latitude = 44.730125, longitude = 39.864296, display_name = "Yuzhnyy"}, ["Veselyy"] = { latitude = 44.676962, longitude = 39.936165, display_name = "Veselyy"}, ["Yuzhnyy"] = { latitude = 45.148146, longitude = 39.027375, display_name = "Yuzhnyy"}, ["Vozdvizhenskaya"] = { latitude = 45.129818, longitude = 40.142530, display_name = "Vozdvizhenskaya"}, ["Zarevo"] = { latitude = 45.002707, longitude = 40.081195, display_name = "Zarevo"}, ["Vozrozhdenie"] = { latitude = 44.549250, longitude = 38.217419, display_name = "Vozrozhdenie"}, ["Vpered"] = { latitude = 44.550175, longitude = 39.705213, display_name = "Vpered"}, ["Zeyuko"] = { latitude = 44.118806, longitude = 41.826530, display_name = "Zeyuko"}, ["Zubi"] = { latitude = 42.571365, longitude = 42.669290, display_name = "Zubi"}, ["Zeda-Gordi"] = { latitude = 42.456377, longitude = 42.522623, display_name = "Zeda-Gordi"}, ["Zeda-Mesheti"] = { latitude = 42.222265, longitude = 42.656102, display_name = "Zeda-Mesheti"}, ["Zeda-Dimi"] = { latitude = 42.073116, longitude = 42.839455, display_name = "Zeda-Dimi"}, ["Zemo-Shuhuti"] = { latitude = 42.081785, longitude = 42.093085, display_name = "Zemo-Shuhuti"}, ["Zeda-Etseri"] = { latitude = 42.081659, longitude = 42.417598, display_name = "Zeda-Etseri"}, ["Zeda-Tsihesulori"] = { latitude = 42.087890, longitude = 42.505130, display_name = "Zeda-Tsihesulori"}, ["Zeindari"] = { latitude = 42.093768, longitude = 42.674916, display_name = "Zeindari"}, ["Zeda-Mukedi"] = { latitude = 42.067746, longitude = 42.468248, display_name = "Zeda-Mukedi"}, ["Zeda-Vani"] = { latitude = 42.066098, longitude = 42.519928, display_name = "Zeda-Vani"}, ["Zeda-Gora"] = { latitude = 42.059853, longitude = 42.690921, display_name = "Zeda-Gora"}, ["Zeda-Zegani"] = { latitude = 42.038936, longitude = 42.921891, display_name = "Zeda-Zegani"}, ["Zemo-Partshma"] = { latitude = 42.036261, longitude = 42.247299, display_name = "Zemo-Partshma"}, ["Zeda-Dzimiti"] = { latitude = 42.013334, longitude = 42.062550, display_name = "Zeda-Dzimiti"}, ["Zovreti"] = { latitude = 42.180543, longitude = 43.040004, display_name = "Zovreti"}, ["Zeda-Bahvi"] = { latitude = 41.947222, longitude = 42.111167, display_name = "Zeda-Bahvi"}, ["Zoti"] = { latitude = 41.893183, longitude = 42.444286, display_name = "Zoti"}, ["Zvare"] = { latitude = 41.982828, longitude = 43.415672, display_name = "Zvare"}, ["Zeni"] = { latitude = 42.520439, longitude = 41.713372, display_name = "Zeni"}, ["Zeni"] = { latitude = 42.384638, longitude = 41.968533, display_name = "Zeni"}, ["Zeda-Etseri"] = { latitude = 42.585548, longitude = 41.920678, display_name = "Zeda-Etseri"}, ["Zeda-Lia"] = { latitude = 42.676335, longitude = 42.023392, display_name = "Zeda-Lia"}, ["Zaragula"] = { latitude = 42.627553, longitude = 42.712001, display_name = "Zaragula"}, ["Zemo-ZHoshkha"] = { latitude = 42.586561, longitude = 42.952919, display_name = "Zemo-ZHoshkha"}, ["Zogishi"] = { latitude = 42.554181, longitude = 42.841330, display_name = "Zogishi"}, ["Zeda-Shavra"] = { latitude = 42.503286, longitude = 42.990015, display_name = "Zeda-Shavra"}, ["Zubi"] = { latitude = 42.397134, longitude = 42.013864, display_name = "Zubi"}, ["Zemo-Huntsi"] = { latitude = 42.407792, longitude = 42.426617, display_name = "Zemo-Huntsi"}, ["Zarati"] = { latitude = 42.355384, longitude = 42.723736, display_name = "Zarati"}, ["Zomleti"] = { latitude = 42.035595, longitude = 42.138993, display_name = "Zomleti"}, ["Zemo-Aketi"] = { latitude = 42.046732, longitude = 42.099893, display_name = "Zemo-Aketi"}, ["Zemo-"] = { latitude = 42.522736, longitude = 43.301035, display_name = "Zemo-"}, ["Zeda-Kveda"] = { latitude = 42.341887, longitude = 43.488780, display_name = "Zeda-Kveda"}, ["Zaarnadzeebi"] = { latitude = 42.256196, longitude = 43.012686, display_name = "Zaarnadzeebi"}, ["Zeda-Beretisa"] = { latitude = 42.195126, longitude = 43.402533, display_name = "Zeda-Beretisa"}, ["Vertkvichala"] = { latitude = 42.105803, longitude = 43.304873, display_name = "Vertkvichala"}, ["Zedubani"] = { latitude = 41.962796, longitude = 43.310404, display_name = "Zedubani"}, ["Zemo-Gumurishi"] = { latitude = 42.710482, longitude = 41.788429, display_name = "Zemo-Gumurishi"}, ["Zeni"] = { latitude = 42.617798, longitude = 41.882572, display_name = "Zeni"}, ["Zeni"] = { latitude = 42.565132, longitude = 41.819593, display_name = "Zeni"}, ["Zeni"] = { latitude = 42.312676, longitude = 41.922268, display_name = "Zeni"}, ["Zemo-Natanebi"] = { latitude = 41.963000, longitude = 41.870221, display_name = "Zemo-Natanebi"}, ["Zeda-Dagva"] = { latitude = 41.754863, longitude = 41.807808, display_name = "Zeda-Dagva"}, ["Zartsupa"] = { latitude = 42.493699, longitude = 41.644697, display_name = "Zartsupa"}, ["ZHoneti"] = { latitude = 42.370197, longitude = 42.703253, display_name = "ZHoneti"}, ["Zeda-Sameba"] = { latitude = 41.791274, longitude = 41.859312, display_name = "Zeda-Sameba"}, ["Verhne-Nikolaevskoe"] = { latitude = 43.529614, longitude = 39.929520, display_name = "Verhne-Nikolaevskoe"}, ["Verhne-Imeretinskaya Buhta"] = { latitude = 43.417052, longitude = 39.975434, display_name = "Verhne-Imeretinskaya Buhta"}, ["Verhne-Armyanskoe Loo"] = { latitude = 43.739337, longitude = 39.617997, display_name = "Verhne-Armyanskoe Loo"}, ["Verh.ZHemtala"] = { latitude = 43.236728, longitude = 43.669495, display_name = "Verh.ZHemtala"}, ["Verh.Teberda"] = { latitude = 43.538315, longitude = 41.782876, display_name = "Verh.Teberda"}, ["Verh.Mara"] = { latitude = 43.771537, longitude = 42.136894, display_name = "Verh.Mara"}, ["Verh.Kurkuzhin"] = { latitude = 43.705952, longitude = 43.296825, display_name = "Verh.Kurkuzhin"}, ["Verh.Chegem"] = { latitude = 43.240698, longitude = 43.134321, display_name = "Verh.Chegem"}, ["Verh.Balkariya"] = { latitude = 43.131049, longitude = 43.456872, display_name = "Verh.Balkariya"}, ["Verh.Baksan"] = { latitude = 43.309417, longitude = 42.750280, display_name = "Verh.Baksan"}, ["Verevkin"] = { latitude = 45.220310, longitude = 40.354313, display_name = "Verevkin"}, ["Velikovechnoe"] = { latitude = 44.936567, longitude = 39.749784, display_name = "Velikovechnoe"}, ["Vazhnoe"] = { latitude = 43.992853, longitude = 41.940704, display_name = "Vazhnoe"}, ["Vasil'evskiy"] = { latitude = 45.091406, longitude = 38.531103, display_name = "Vasil'evskiy"}, ["Varvarovka"] = { latitude = 44.834052, longitude = 37.372957, display_name = "Varvarovka"}, ["Vartsihe"] = { latitude = 42.144134, longitude = 42.718334, display_name = "Vartsihe"}, ["Varnavinskoe"] = { latitude = 44.995514, longitude = 38.192444, display_name = "Varnavinskoe"}, ["Varenikovskaya"] = { latitude = 45.120464, longitude = 37.634587, display_name = "Varenikovskaya"}, ["Vardane"] = { latitude = 43.734603, longitude = 39.553328, display_name = "Vardane"}, ["Vani"] = { latitude = 41.986843, longitude = 43.203182, display_name = "Vani"}, ["Vani"] = { latitude = 42.048170, longitude = 42.149633, display_name = "Vani"}, ["Vakidzhvari"] = { latitude = 41.915906, longitude = 42.141490, display_name = "Vakidzhvari"}, ["Vachevi"] = { latitude = 42.319197, longitude = 43.198124, display_name = "Vachevi"}, ["Utsera"] = { latitude = 42.633276, longitude = 43.538861, display_name = "Utsera"}, ["Utash"] = { latitude = 45.097694, longitude = 37.319662, display_name = "Utash"}, ["Usahelo"] = { latitude = 42.597293, longitude = 42.828361, display_name = "Usahelo"}, ["Usahelo"] = { latitude = 42.232442, longitude = 43.371765, display_name = "Usahelo"}, ["Urvani"] = { latitude = 42.642964, longitude = 43.282974, display_name = "Urvani"}, ["Urvan'"] = { latitude = 43.490777, longitude = 43.764370, display_name = "Urvan'"}, ["Urup"] = { latitude = 43.846970, longitude = 41.152495, display_name = "Urup"}, ["Uruhskaya"] = { latitude = 44.153716, longitude = 43.669649, display_name = "Uruhskaya"}, ["Urta"] = { latitude = 42.430754, longitude = 41.841157, display_name = "Urta"}, ["Urozhaynyy"] = { latitude = 44.116948, longitude = 42.762639, display_name = "Urozhaynyy"}, ["Ureki"] = { latitude = 41.994268, longitude = 41.778543, display_name = "Ureki"}, ["Ulyap"] = { latitude = 45.055071, longitude = 39.950905, display_name = "Ulyap"}, ["Uluria"] = { latitude = 42.585800, longitude = 42.113808, display_name = "Uluria"}, ["Ul'yanovka"] = { latitude = 44.303471, longitude = 42.927167, display_name = "Ul'yanovka"}, ["Ukrainskiy"] = { latitude = 45.254040, longitude = 39.354076, display_name = "Ukrainskiy"}, ["Ukanava"] = { latitude = 41.934585, longitude = 42.186210, display_name = "Ukanava"}, ["Uhuti"] = { latitude = 42.040771, longitude = 42.683152, display_name = "Uhuti"}, ["Udobnaya"] = { latitude = 44.214925, longitude = 41.562212, display_name = "Udobnaya"}, ["Uchkulan"] = { latitude = 43.455180, longitude = 42.087454, display_name = "Uchkulan"}, ["Uchashona"] = { latitude = 42.493247, longitude = 41.924453, display_name = "Uchashona"}, ["Ubisi"] = { latitude = 42.098775, longitude = 43.226444, display_name = "Ubisi"}, ["Ubinskaya"] = { latitude = 44.737000, longitude = 38.541069, display_name = "Ubinskaya"}, ["Uazabaa"] = { latitude = 43.054593, longitude = 40.976749, display_name = "Uazabaa"}, ["Tyumenskiy"] = { latitude = 44.182244, longitude = 38.972674, display_name = "Tyumenskiy"}, ["Tvrini"] = { latitude = 42.073237, longitude = 43.072609, display_name = "Tvrini"}, ["Tvishi"] = { latitude = 42.515837, longitude = 42.788506, display_name = "Tvishi"}, ["Tverskaya"] = { latitude = 44.604805, longitude = 39.610687, display_name = "Tverskaya"}, ["Tvalueti"] = { latitude = 42.227705, longitude = 43.268895, display_name = "Tvalueti"}, ["Tuzi"] = { latitude = 42.272471, longitude = 43.110811, display_name = "Tuzi"}, ["Tsvirmi"] = { latitude = 43.017141, longitude = 42.801118, display_name = "Tsvirmi"}, ["Tsvane"] = { latitude = 42.370055, longitude = 41.654112, display_name = "Tsvane"}, ["Tsutshvati"] = { latitude = 42.289077, longitude = 42.860776, display_name = "Tsutshvati"}, ["Tskrysh"] = { latitude = 42.782967, longitude = 41.373482, display_name = "Tskrysh"}, ["Tsknori"] = { latitude = 42.402082, longitude = 42.890375, display_name = "Tsknori"}, ["Tskemi"] = { latitude = 42.269262, longitude = 42.174364, display_name = "Tskemi"}, ["Tskavroka"] = { latitude = 41.859784, longitude = 41.888202, display_name = "Tskavroka"}, ["Tskaltsminda"] = { latitude = 42.011212, longitude = 41.778545, display_name = "Tskaltsminda"}, ["Tskaltashua"] = { latitude = 42.022671, longitude = 42.828904, display_name = "Tskaltashua"}, ["Tskalshavi"] = { latitude = 42.219307, longitude = 43.341725, display_name = "Tskalshavi"}, ["Tskalaporeti"] = { latitude = 42.031419, longitude = 43.078673, display_name = "Tskalaporeti"}, ["Tsipnara"] = { latitude = 42.024010, longitude = 42.293819, display_name = "Tsipnara"}, ["Tsipnagvara"] = { latitude = 41.955727, longitude = 42.224549, display_name = "Tsipnagvara"}, ["Tsiperchi"] = { latitude = 42.603636, longitude = 42.695129, display_name = "Tsiperchi"}, ["Tsipa"] = { latitude = 42.046461, longitude = 42.949247, display_name = "Tsipa"}, ["Tsipa"] = { latitude = 42.009347, longitude = 43.451125, display_name = "Tsipa"}, ["Tsihisdziri"] = { latitude = 41.772133, longitude = 41.762802, display_name = "Tsihisdziri"}, ["Tsibanobalka"] = { latitude = 44.982008, longitude = 37.342091, display_name = "Tsibanobalka"}, ["Tshunkuri"] = { latitude = 42.396804, longitude = 42.570436, display_name = "Tshunkuri"}, ["Tshmori"] = { latitude = 42.536955, longitude = 43.478057, display_name = "Tshmori"}, ["Tshentaro"] = { latitude = 42.147873, longitude = 42.837507, display_name = "Tshentaro"}, ["Tshenis-Tskali"] = { latitude = 42.796462, longitude = 41.419871, display_name = "Tshenis-Tskali"}, ["Tshemlishidi"] = { latitude = 41.921541, longitude = 42.071692, display_name = "Tshemlishidi"}, ["Tshami"] = { latitude = 42.291453, longitude = 43.510570, display_name = "Tshami"}, ["Tsesi"] = { latitude = 42.542767, longitude = 43.196616, display_name = "Tsesi"}, ["Tsedisi"] = { latitude = 42.532887, longitude = 43.548207, display_name = "Tsedisi"}, ["Tsebel'da"] = { latitude = 43.022159, longitude = 41.269029, display_name = "Tsebel'da"}, ["Tsatshvi"] = { latitude = 42.414906, longitude = 41.789573, display_name = "Tsatshvi"}, ["Tsalkoti"] = { latitude = 43.407001, longitude = 40.052620, display_name = "Tsalkoti"}, ["Tsaishi"] = { latitude = 42.431382, longitude = 41.803488, display_name = "Tsaishi"}, ["Tsahi"] = { latitude = 42.526030, longitude = 42.922673, display_name = "Tsahi"}, ["Trudobelikovskiy"] = { latitude = 45.285178, longitude = 38.142705, display_name = "Trudobelikovskiy"}, ["Travlev"] = { latitude = 44.370687, longitude = 39.537629, display_name = "Travlev"}, ["Tolebi"] = { latitude = 42.074187, longitude = 42.240400, display_name = "Tolebi"}, ["Tkviri"] = { latitude = 42.169336, longitude = 42.246736, display_name = "Tkviri"}, ["Tklapivake"] = { latitude = 42.172575, longitude = 43.068294, display_name = "Tklapivake"}, ["Tkelvani"] = { latitude = 42.047842, longitude = 42.551287, display_name = "Tkelvani"}, ["Tkaya"] = { latitude = 42.622523, longitude = 41.928621, display_name = "Tkaya"}, ["Tkachiri"] = { latitude = 42.142229, longitude = 42.632747, display_name = "Tkachiri"}, ["Tihovskiy"] = { latitude = 45.194706, longitude = 38.218835, display_name = "Tihovskiy"}, ["Thmelari"] = { latitude = 42.145418, longitude = 42.263166, display_name = "Thmelari"}, ["Thina"] = { latitude = 42.881268, longitude = 41.544684, display_name = "Thina"}, ["Thilnari"] = { latitude = 41.569953, longitude = 41.647305, display_name = "Thilnari"}, ["Teuchezhkhabl'"] = { latitude = 44.931656, longitude = 39.562499, display_name = "Teuchezhkhabl'"}, ["Tenginskaya"] = { latitude = 45.100953, longitude = 40.008266, display_name = "Tenginskaya"}, ["Tenginka"] = { latitude = 44.328982, longitude = 38.783412, display_name = "Tenginka"}, ["Telmani"] = { latitude = 42.041496, longitude = 42.055072, display_name = "Telmani"}, ["Telepa"] = { latitude = 42.199325, longitude = 43.013716, display_name = "Telepa"}, ["Teklati"] = { latitude = 42.253536, longitude = 42.009190, display_name = "Teklati"}, ["Taya"] = { latitude = 42.610109, longitude = 42.213082, display_name = "Taya"}, ["Tavisupleba"] = { latitude = 43.041709, longitude = 41.016536, display_name = "Tavisupleba"}, ["Tavasa"] = { latitude = 42.275571, longitude = 43.082456, display_name = "Tavasa"}, ["Tashly-Tala"] = { latitude = 43.146908, longitude = 43.697247, display_name = "Tashly-Tala"}, ["Tambukan"] = { latitude = 43.932197, longitude = 43.142965, display_name = "Tambukan"}, ["Tamanskiy"] = { latitude = 45.150180, longitude = 36.782611, display_name = "Tamanskiy"}, ["Taman'"] = { latitude = 45.211080, longitude = 36.712001, display_name = "Taman'"}, ["Tamakoni"] = { latitude = 42.458194, longitude = 42.305573, display_name = "Tamakoni"}, ["Tallyk"] = { latitude = 44.153193, longitude = 42.343567, display_name = "Tallyk"}, ["Tahtamukay"] = { latitude = 44.922632, longitude = 38.994206, display_name = "Tahtamukay"}, ["Tagiloni"] = { latitude = 42.547563, longitude = 41.772461, display_name = "Tagiloni"}, ["Tabori"] = { latitude = 42.605990, longitude = 42.892410, display_name = "Tabori"}, ["Tabakini"] = { latitude = 42.065905, longitude = 43.036069, display_name = "Tabakini"}, ["Tabagrebi"] = { latitude = 42.328721, longitude = 43.275940, display_name = "Tabagrebi"}, ["Tabachnyy"] = { latitude = 44.556422, longitude = 40.091093, display_name = "Tabachnyy"}, ["Svoboda"] = { latitude = 44.155825, longitude = 42.768210, display_name = "Svoboda"}, ["svh.Tagrskiy"] = { latitude = 43.199276, longitude = 40.284213, display_name = "svh.Tagrskiy"}, ["svh.Nasakirali"] = { latitude = 41.970377, longitude = 42.020224, display_name = "svh.Nasakirali"}, ["svh.Kohora"] = { latitude = 42.659065, longitude = 41.708095, display_name = "svh.Kohora"}, ["svh.Horshi"] = { latitude = 42.341095, longitude = 42.035080, display_name = "svh.Horshi"}, ["svh.Didi-Chkonskiy"] = { latitude = 42.539708, longitude = 42.290066, display_name = "svh.Didi-Chkonskiy"}, ["svh.Ahalsopeli"] = { latitude = 42.249177, longitude = 41.857919, display_name = "svh.Ahalsopeli"}, ["Svetlyy Put' Lenina"] = { latitude = 45.208583, longitude = 37.656995, display_name = "Svetlyy Put' Lenina"}, ["Svetlovodskoe"] = { latitude = 43.896544, longitude = 43.180565, display_name = "Svetlovodskoe"}, ["Sveri"] = { latitude = 42.226188, longitude = 43.303560, display_name = "Sveri"}, ["Suzdal'skaya"] = { latitude = 44.766994, longitude = 39.368694, display_name = "Suzdal'skaya"}, ["Suvorovskoe"] = { latitude = 45.286566, longitude = 39.460538, display_name = "Suvorovskoe"}, ["Suvorovskaya"] = { latitude = 44.187846, longitude = 42.658805, display_name = "Suvorovskaya"}, ["Suvorov-Cherkesskiy"] = { latitude = 45.069861, longitude = 37.271582, display_name = "Suvorov-Cherkesskiy"}, ["Surmushi"] = { latitude = 42.592088, longitude = 42.878475, display_name = "Surmushi"}, ["Supseh"] = { latitude = 44.861146, longitude = 37.364340, display_name = "Supseh"}, ["Supovskiy"] = { latitude = 44.902356, longitude = 38.942116, display_name = "Supovskiy"}, ["Sulori"] = { latitude = 42.025049, longitude = 42.577617, display_name = "Sulori"}, ["Sukko"] = { latitude = 44.798695, longitude = 37.420789, display_name = "Sukko"}, ["Suhoy Kut"] = { latitude = 45.127745, longitude = 40.207372, display_name = "Suhoy Kut"}, ["Suhcha"] = { latitude = 42.417076, longitude = 42.468544, display_name = "Suhcha"}, ["Sudzhuna"] = { latitude = 42.200960, longitude = 42.146446, display_name = "Sudzhuna"}, ["Strelka"] = { latitude = 45.204296, longitude = 37.286910, display_name = "Strelka"}, ["Storozhevaya"] = { latitude = 43.883112, longitude = 41.453295, display_name = "Storozhevaya"}, ["Stavropol'skaya"] = { latitude = 44.718327, longitude = 38.825185, display_name = "Stavropol'skaya"}, ["Starotitarovskaya"] = { latitude = 45.219384, longitude = 37.161638, display_name = "Starotitarovskaya"}, ["Staropavlovskaya"] = { latitude = 43.846862, longitude = 43.635628, display_name = "Staropavlovskaya"}, ["Starokorsunskaya"] = { latitude = 45.056256, longitude = 39.314748, display_name = "Starokorsunskaya"}, ["Starobzhegokay"] = { latitude = 45.037835, longitude = 38.890909, display_name = "Starobzhegokay"}, ["Star.Cherek"] = { latitude = 43.474391, longitude = 43.858173, display_name = "Star.Cherek"}, ["Stantsionnyy"] = { latitude = 44.438859, longitude = 39.474111, display_name = "Stantsionnyy"}, ["Spokoynaya"] = { latitude = 44.259312, longitude = 41.390173, display_name = "Spokoynaya"}, ["Speti"] = { latitude = 42.322001, longitude = 43.523397, display_name = "Speti"}, ["Spatagori"] = { latitude = 42.612449, longitude = 42.820431, display_name = "Spatagori"}, ["Sovhoznyy"] = { latitude = 44.543971, longitude = 40.152743, display_name = "Sovhoznyy"}, ["Sovhoznyy"] = { latitude = 45.297970, longitude = 38.106763, display_name = "Sovhoznyy"}, ["Sovhoznoe"] = { latitude = 43.803855, longitude = 43.147584, display_name = "Sovhoznoe"}, ["Sormoni"] = { latitude = 42.320448, longitude = 42.736131, display_name = "Sormoni"}, ["Soloniki"] = { latitude = 43.884949, longitude = 39.378103, display_name = "Soloniki"}, ["Solenoe"] = { latitude = 44.044307, longitude = 40.872960, display_name = "Solenoe"}, ["Soldatskaya"] = { latitude = 43.812310, longitude = 43.823078, display_name = "Soldatskaya"}, ["Soglasnyy"] = { latitude = 45.237391, longitude = 39.975613, display_name = "Soglasnyy"}, ["Sochkheti"] = { latitude = 42.395196, longitude = 42.910288, display_name = "Sochkheti"}, ["Smolenskaya"] = { latitude = 44.786688, longitude = 38.800768, display_name = "Smolenskaya"}, ["Skurdi"] = { latitude = 42.476765, longitude = 42.368497, display_name = "Skurdi"}, ["Skura"] = { latitude = 41.866696, longitude = 41.925186, display_name = "Skura"}, ["Skindori"] = { latitude = 42.242029, longitude = 43.249227, display_name = "Skindori"}, ["Sinegorsk"] = { latitude = 44.775835, longitude = 38.338404, display_name = "Sinegorsk"}, ["Simoneti"] = { latitude = 42.238107, longitude = 42.912135, display_name = "Simoneti"}, ["Siktarva"] = { latitude = 42.192302, longitude = 42.940934, display_name = "Siktarva"}, ["Sida"] = { latitude = 42.570767, longitude = 41.712233, display_name = "Sida"}, ["Shuntuk"] = { latitude = 44.455608, longitude = 40.164758, display_name = "Shuntuk"}, ["Shuamta"] = { latitude = 42.097609, longitude = 42.451121, display_name = "Shuamta"}, ["Shua-shvava"] = { latitude = 42.501499, longitude = 43.221855, display_name = "Shua-shvava"}, ["Shua-Nosiri"] = { latitude = 42.280291, longitude = 42.094655, display_name = "Shua-Nosiri"}, ["Shua-Gubi"] = { latitude = 42.261628, longitude = 42.466473, display_name = "Shua-Gubi"}, ["Shua-Gezruli"] = { latitude = 42.153417, longitude = 43.219613, display_name = "Shua-Gezruli"}, ["Shua-Bashi"] = { latitude = 42.143434, longitude = 42.493907, display_name = "Shua-Bashi"}, ["Shturbino"] = { latitude = 45.054728, longitude = 39.912252, display_name = "Shturbino"}, ["Shrosha"] = { latitude = 42.115040, longitude = 43.179695, display_name = "Shrosha"}, ["Shromiskari"] = { latitude = 42.358092, longitude = 42.116093, display_name = "Shromiskari"}, ["Shroma"] = { latitude = 42.522152, longitude = 43.061884, display_name = "Shroma"}, ["Shroma"] = { latitude = 43.082720, longitude = 41.035309, display_name = "Shroma"}, ["Shovgenovskiy"] = { latitude = 45.021933, longitude = 40.235745, display_name = "Shovgenovskiy"}, ["Shomaheti"] = { latitude = 42.214857, longitude = 43.428888, display_name = "Shomaheti"}, ["Shkol'nyy"] = { latitude = 45.034098, longitude = 37.615478, display_name = "Shkol'nyy"}, ["Shkol'noe"] = { latitude = 44.916928, longitude = 39.856176, display_name = "Shkol'noe"}, ["Shitskvara"] = { latitude = 43.070784, longitude = 40.927797, display_name = "Shitskvara"}, ["Shithala"] = { latitude = 43.555555, longitude = 43.798022, display_name = "Shithala"}, ["Shirvanskaya"] = { latitude = 44.380256, longitude = 39.807018, display_name = "Shirvanskaya"}, ["Shirokaya Balka"] = { latitude = 44.487080, longitude = 39.412907, display_name = "Shirokaya Balka"}, ["Shevchenko"] = { latitude = 44.899509, longitude = 39.516976, display_name = "Shevchenko"}, ["Shepsi"] = { latitude = 44.038265, longitude = 39.141680, display_name = "Shepsi"}, ["Shendzhiy"] = { latitude = 44.887778, longitude = 39.060568, display_name = "Shendzhiy"}, ["Shedok"] = { latitude = 44.221288, longitude = 40.837051, display_name = "Shedok"}, ["Shaumyanskiy"] = { latitude = 44.163229, longitude = 43.539348, display_name = "Shaumyanskiy"}, ["Shaumyan"] = { latitude = 44.323123, longitude = 39.290108, display_name = "Shaumyan"}, ["Shardakovo"] = { latitude = 43.879279, longitude = 43.102586, display_name = "Shardakovo"}, ["Shamgona"] = { latitude = 42.521753, longitude = 41.769134, display_name = "Shamgona"}, ["Shalushka"] = { latitude = 43.530601, longitude = 43.568470, display_name = "Shalushka"}, ["Shahe"] = { latitude = 43.789565, longitude = 39.474887, display_name = "Shahe"}, ["Severskaya"] = { latitude = 44.854150, longitude = 38.678560, display_name = "Severskaya"}, ["Severnyy"] = { latitude = 44.658857, longitude = 40.117073, display_name = "Severnyy"}, ["Sevastopol'skaya"] = { latitude = 44.354847, longitude = 40.303966, display_name = "Sevastopol'skaya"}, ["Seva"] = { latitude = 42.555444, longitude = 43.351597, display_name = "Seva"}, ["Sergieti"] = { latitude = 42.391498, longitude = 42.323468, display_name = "Sergieti"}, ["Sepieti"] = { latitude = 42.281965, longitude = 42.243453, display_name = "Sepieti"}, ["Sennoy"] = { latitude = 45.294518, longitude = 36.989450, display_name = "Sennoy"}, ["Semisvodnyy"] = { latitude = 45.300754, longitude = 37.969587, display_name = "Semisvodnyy"}, ["Schastlivoe"] = { latitude = 44.164789, longitude = 42.271165, display_name = "Schastlivoe"}, ["Sazano"] = { latitude = 42.218143, longitude = 43.062941, display_name = "Sazano"}, ["Savane"] = { latitude = 42.315460, longitude = 43.467280, display_name = "Savane"}, ["Saukdere"] = { latitude = 44.903911, longitude = 37.885750, display_name = "Saukdere"}, ["Satkebuchao"] = { latitude = 42.402993, longitude = 42.058518, display_name = "Satkebuchao"}, ["Sashamugio"] = { latitude = 42.580189, longitude = 41.658063, display_name = "Sashamugio"}, ["Sasashi"] = { latitude = 42.800320, longitude = 42.978569, display_name = "Sasashi"}, ["Sary-Tyuz"] = { latitude = 43.902060, longitude = 41.893315, display_name = "Sary-Tyuz"}, ["Sarmakovo"] = { latitude = 43.741446, longitude = 43.196640, display_name = "Sarmakovo"}, ["Sareki"] = { latitude = 42.330518, longitude = 43.359819, display_name = "Sareki"}, ["Saratovskiy"] = { latitude = 45.212657, longitude = 39.980321, display_name = "Saratovskiy"}, ["Saratovskiy"] = { latitude = 45.101815, longitude = 39.762800, display_name = "Saratovskiy"}, ["Saratovskaya"] = { latitude = 44.711278, longitude = 39.218458, display_name = "Saratovskaya"}, ["Sarakoni"] = { latitude = 42.503838, longitude = 42.058830, display_name = "Sarakoni"}, ["Saprasia"] = { latitude = 42.037612, longitude = 42.645146, display_name = "Saprasia"}, ["Saodishario"] = { latitude = 42.246902, longitude = 42.114695, display_name = "Saodishario"}, ["San.Im.Lenina"] = { latitude = 42.926139, longitude = 41.115398, display_name = "San.Im.Lenina"}, ["Samikao"] = { latitude = 42.263550, longitude = 42.296038, display_name = "Samikao"}, ["Samelalio"] = { latitude = 42.707453, longitude = 41.749466, display_name = "Samelalio"}, ["Salominao"] = { latitude = 42.088135, longitude = 42.719745, display_name = "Salominao"}, ["Salieti"] = { latitude = 42.271758, longitude = 43.209060, display_name = "Salieti"}, ["Salhino"] = { latitude = 43.486466, longitude = 40.054114, display_name = "Salhino"}, ["Salhino"] = { latitude = 42.515468, longitude = 42.341970, display_name = "Salhino"}, ["Sal'me"] = { latitude = 43.427666, longitude = 40.024911, display_name = "Sal'me"}, ["Sakulia"] = { latitude = 42.134432, longitude = 42.561555, display_name = "Sakulia"}, ["Sakraula"] = { latitude = 42.031286, longitude = 42.984628, display_name = "Sakraula"}, ["Saketsia"] = { latitude = 42.529619, longitude = 43.087889, display_name = "Saketsia"}, ["Sairhe"] = { latitude = 42.313435, longitude = 43.408683, display_name = "Sairhe"}, ["Sagvichio"] = { latitude = 42.213265, longitude = 41.874584, display_name = "Sagvichio"}, ["Sagvamichavo"] = { latitude = 42.228483, longitude = 41.827094, display_name = "Sagvamichavo"}, ["Saeliavo"] = { latitude = 42.437401, longitude = 42.387708, display_name = "Saeliavo"}, ["Sadovyy"] = { latitude = 45.319973, longitude = 38.053711, display_name = "Sadovyy"}, ["Sadovyy"] = { latitude = 44.227462, longitude = 43.195699, display_name = "Sadovyy"}, ["Sadovyy"] = { latitude = 45.018190, longitude = 37.747990, display_name = "Sadovyy"}, ["Sadovoe"] = { latitude = 45.003356, longitude = 39.693078, display_name = "Sadovoe"}, ["Sadovoe"] = { latitude = 44.332447, longitude = 42.038087, display_name = "Sadovoe"}, ["Sadovoe"] = { latitude = 44.018527, longitude = 42.972176, display_name = "Sadovoe"}, ["Sadmeli"] = { latitude = 42.540139, longitude = 43.114993, display_name = "Sadmeli"}, ["Sachochuo"] = { latitude = 42.209857, longitude = 41.766596, display_name = "Sachochuo"}, ["Sachino"] = { latitude = 42.647290, longitude = 42.083729, display_name = "Sachino"}, ["Sabuliskerio"] = { latitude = 42.675257, longitude = 41.675785, display_name = "Sabuliskerio"}, ["Saberio"] = { latitude = 42.645920, longitude = 41.907494, display_name = "Saberio"}, ["Sabe"] = { latitude = 42.046961, longitude = 43.251094, display_name = "Sabe"}, ["Sabazho"] = { latitude = 42.222720, longitude = 41.798622, display_name = "Sabazho"}, ["Ryazanskaya"] = { latitude = 44.961990, longitude = 39.578809, display_name = "Ryazanskaya"}, ["Russkoe"] = { latitude = 44.961940, longitude = 37.836869, display_name = "Russkoe"}, ["Russkaya Mamayka"] = { latitude = 43.650247, longitude = 39.714431, display_name = "Russkaya Mamayka"}, ["Ruhi"] = { latitude = 42.544316, longitude = 41.847535, display_name = "Ruhi"}, ["Rtshilati"] = { latitude = 42.325175, longitude = 43.134800, display_name = "Rtshilati"}, ["Roschinskiy"] = { latitude = 44.383885, longitude = 42.148785, display_name = "Roschinskiy"}, ["Rohi"] = { latitude = 42.107479, longitude = 42.706407, display_name = "Rohi"}, ["Rodnikovyy"] = { latitude = 44.683532, longitude = 39.999878, display_name = "Rodnikovyy"}, ["Rodniki"] = { latitude = 44.740312, longitude = 39.912532, display_name = "Rodniki"}, ["Rodinauli"] = { latitude = 42.150130, longitude = 42.871330, display_name = "Rodinauli"}, ["Rioni"] = { latitude = 42.332956, longitude = 42.714852, display_name = "Rioni"}, ["Rim-Gorskiy"] = { latitude = 43.958096, longitude = 42.524108, display_name = "Rim-Gorskiy"}, ["Rike"] = { latitude = 42.588840, longitude = 41.890551, display_name = "Rike"}, ["Repo-Etseri"] = { latitude = 42.631793, longitude = 41.658992, display_name = "Repo-Etseri"}, ["Rechkhi"] = { latitude = 42.659807, longitude = 41.747882, display_name = "Rechkhi"}, ["Razdol'noe"] = { latitude = 43.592801, longitude = 39.798031, display_name = "Razdol'noe"}, ["Rassvet"] = { latitude = 44.898001, longitude = 37.454519, display_name = "Rassvet"}, ["Raevskaya"] = { latitude = 44.835329, longitude = 37.559541, display_name = "Raevskaya"}, ["Pyatihatki"] = { latitude = 44.976064, longitude = 37.304959, display_name = "Pyatihatki"}, ["Pyatigorskiy"] = { latitude = 43.974984, longitude = 43.261010, display_name = "Pyatigorskiy"}, ["Psyzh"] = { latitude = 44.241128, longitude = 42.022016, display_name = "Psyzh"}, ["Psyrtsha"] = { latitude = 43.087430, longitude = 40.879180, display_name = "Psyrtsha"}, ["Psynshoko"] = { latitude = 43.758797, longitude = 43.716771, display_name = "Psynshoko"}, ["Psynodaha"] = { latitude = 43.861800, longitude = 43.244666, display_name = "Psynodaha"}, ["Psyhurey"] = { latitude = 43.838650, longitude = 43.577387, display_name = "Psyhurey"}, ["Psygansu"] = { latitude = 43.415515, longitude = 43.789113, display_name = "Psygansu"}, ["Psychoh"] = { latitude = 43.718672, longitude = 43.525482, display_name = "Psychoh"}, ["Pshizov"] = { latitude = 45.095471, longitude = 40.104028, display_name = "Pshizov"}, ["Pshicho"] = { latitude = 45.066198, longitude = 40.173833, display_name = "Pshicho"}, ["Pshehskaya"] = { latitude = 44.698485, longitude = 39.790475, display_name = "Pshehskaya"}, ["Pshap"] = { latitude = 42.900098, longitude = 41.103230, display_name = "Pshap"}, ["Pseytuh"] = { latitude = 45.053705, longitude = 38.704020, display_name = "Pseytuh"}, ["Psemen"] = { latitude = 43.983633, longitude = 40.975741, display_name = "Psemen"}, ["Psekups"] = { latitude = 44.835444, longitude = 39.207383, display_name = "Psekups"}, ["Psauch'e-Dahe"] = { latitude = 44.215163, longitude = 41.879390, display_name = "Psauch'e-Dahe"}, ["Protichka"] = { latitude = 45.378976, longitude = 38.070425, display_name = "Protichka"}, ["Proletarskiy"] = { latitude = 44.619952, longitude = 40.181767, display_name = "Proletarskiy"}, ["Progress"] = { latitude = 43.818331, longitude = 43.335369, display_name = "Progress"}, ["Progress"] = { latitude = 44.871644, longitude = 40.189500, display_name = "Progress"}, ["Privol'noe"] = { latitude = 44.006071, longitude = 42.941312, display_name = "Privol'noe"}, ["Prirechnoe"] = { latitude = 43.803274, longitude = 43.305037, display_name = "Prirechnoe"}, ["Prirechenskiy"] = { latitude = 44.755341, longitude = 39.241202, display_name = "Prirechenskiy"}, ["Primorskoe"] = { latitude = 43.094709, longitude = 40.704462, display_name = "Primorskoe"}, ["Primorskiy"] = { latitude = 45.264384, longitude = 36.909279, display_name = "Primorskiy"}, ["Prikubanskiy"] = { latitude = 44.958409, longitude = 39.027413, display_name = "Prikubanskiy"}, ["Prikubanskie hutora"] = { latitude = 45.161087, longitude = 38.072122, display_name = "Prikubanskie hutora"}, ["Prigorodnyy"] = { latitude = 44.124347, longitude = 39.118629, display_name = "Prigorodnyy"}, ["Prigorodnoe"] = { latitude = 44.238742, longitude = 42.120034, display_name = "Prigorodnoe"}, ["Preobrazhenskoe"] = { latitude = 45.087444, longitude = 39.623111, display_name = "Preobrazhenskoe"}, ["Pregradnaya"] = { latitude = 43.951487, longitude = 41.184826, display_name = "Pregradnaya"}, ["Pravokubanskiy"] = { latitude = 43.919953, longitude = 41.883161, display_name = "Pravokubanskiy"}, ["Potsho"] = { latitude = 42.432617, longitude = 42.167673, display_name = "Potsho"}, ["Ponezhukay"] = { latitude = 44.891919, longitude = 39.381016, display_name = "Ponezhukay"}, ["Pokvesh"] = { latitude = 42.797212, longitude = 41.568194, display_name = "Pokvesh"}, ["Pokrovskiy"] = { latitude = 45.111818, longitude = 38.415735, display_name = "Pokrovskiy"}, ["Podkumok"] = { latitude = 43.970835, longitude = 42.775149, display_name = "Podkumok"}, ["Podgornyy"] = { latitude = 44.692614, longitude = 40.083808, display_name = "Podgornyy"}, ["Podgornaya"] = { latitude = 44.214940, longitude = 41.283165, display_name = "Podgornaya"}, ["Pobegaylovka"] = { latitude = 44.241492, longitude = 43.014814, display_name = "Pobegaylovka"}, ["Plavnenskiy"] = { latitude = 45.036542, longitude = 37.930622, display_name = "Plavnenskiy"}, ["Plastunovskaya"] = { latitude = 45.296667, longitude = 39.265353, display_name = "Plastunovskaya"}, ["Plastunka"] = { latitude = 43.671607, longitude = 39.761316, display_name = "Plastunka"}, ["Pitsargali"] = { latitude = 42.533189, longitude = 41.596891, display_name = "Pitsargali"}, ["Pirveli-Tola"] = { latitude = 42.573555, longitude = 42.984611, display_name = "Pirveli-Tola"}, ["Pirveli-Sviri"] = { latitude = 42.106504, longitude = 42.960607, display_name = "Pirveli-Sviri"}, ["Pirveli-Ontopo"] = { latitude = 42.250454, longitude = 42.246871, display_name = "Pirveli-Ontopo"}, ["Pirveli-Obcha"] = { latitude = 42.097058, longitude = 42.857916, display_name = "Pirveli-Obcha"}, ["Pirveli-Maisi"] = { latitude = 42.101253, longitude = 43.085303, display_name = "Pirveli-Maisi"}, ["Pirveli-Gurdzemi"] = { latitude = 42.469479, longitude = 42.265483, display_name = "Pirveli-Gurdzemi"}, ["Pirveli-Gudava"] = { latitude = 42.662997, longitude = 41.558282, display_name = "Pirveli-Gudava"}, ["Pirveli-Etseri"] = { latitude = 42.183070, longitude = 42.158404, display_name = "Pirveli-Etseri"}, ["Pirveli-Choga"] = { latitude = 42.574126, longitude = 42.198176, display_name = "Pirveli-Choga"}, ["Pirveli-Akvaga"] = { latitude = 42.602194, longitude = 41.772756, display_name = "Pirveli-Akvaga"}, ["Pervorechenskoe"] = { latitude = 45.164385, longitude = 39.308994, display_name = "Pervorechenskoe"}, ["Pervomayskiy"] = { latitude = 45.180051, longitude = 38.305376, display_name = "Pervomayskiy"}, ["Pervomayskiy"] = { latitude = 44.897833, longitude = 39.739162, display_name = "Pervomayskiy"}, ["Pervomayskiy"] = { latitude = 44.694004, longitude = 39.306035, display_name = "Pervomayskiy"}, ["Pervomayskiy"] = { latitude = 44.408531, longitude = 40.186120, display_name = "Pervomayskiy"}, ["Perveli-Ohurey"] = { latitude = 42.736365, longitude = 41.575883, display_name = "Perveli-Ohurey"}, ["Persati"] = { latitude = 42.068078, longitude = 42.788478, display_name = "Persati"}, ["Perevisa"] = { latitude = 42.260370, longitude = 43.294406, display_name = "Perevisa"}, ["Perevalka"] = { latitude = 44.053160, longitude = 40.756944, display_name = "Perevalka"}, ["Pereta"] = { latitude = 42.062818, longitude = 42.715368, display_name = "Pereta"}, ["Peredovaya"] = { latitude = 44.119929, longitude = 41.476287, display_name = "Peredovaya"}, ["Pchegatlukay"] = { latitude = 44.888976, longitude = 39.262451, display_name = "Pchegatlukay"}, ["Pavlovskoe"] = { latitude = 43.067519, longitude = 41.114584, display_name = "Pavlovskoe"}, ["Pavlovskiy"] = { latitude = 45.078137, longitude = 37.772047, display_name = "Pavlovskiy"}, ["Patriketi"] = { latitude = 42.156221, longitude = 42.656848, display_name = "Patriketi"}, ["Patara-Poti"] = { latitude = 42.190478, longitude = 41.731759, display_name = "Patara-Poti"}, ["Patara-Oni"] = { latitude = 42.535801, longitude = 42.981056, display_name = "Patara-Oni"}, ["Patara-Dzhihaishi"] = { latitude = 42.284101, longitude = 42.404035, display_name = "Patara-Dzhihaishi"}, ["Partshnali"] = { latitude = 42.007781, longitude = 43.124335, display_name = "Partshnali"}, ["Partshanakanevi"] = { latitude = 42.207220, longitude = 42.557555, display_name = "Partshanakanevi"}, ["Partonohori"] = { latitude = 42.651198, longitude = 41.861593, display_name = "Partonohori"}, ["Paraheti"] = { latitude = 42.553093, longitude = 43.320122, display_name = "Paraheti"}, ["Panahes"] = { latitude = 44.987973, longitude = 38.713089, display_name = "Panahes"}, ["Pahulani"] = { latitude = 42.654425, longitude = 41.989393, display_name = "Pahulani"}, ["Otradnyy"] = { latitude = 44.874473, longitude = 38.952471, display_name = "Otradnyy"}, ["Otradnoe"] = { latitude = 43.265661, longitude = 40.288506, display_name = "Otradnoe"}, ["otd.N3 SKZNIISiV"] = { latitude = 45.136100, longitude = 38.965099, display_name = "otd.N3 SKZNIISiV"}, ["Ostrovskaya Shel'"] = { latitude = 44.278869, longitude = 39.291550, display_name = "Ostrovskaya Shel'"}, ["Ostrogorka"] = { latitude = 44.123258, longitude = 42.979051, display_name = "Ostrogorka"}, ["Orsantia"] = { latitude = 42.470203, longitude = 41.669408, display_name = "Orsantia"}, ["Orpiri"] = { latitude = 42.332106, longitude = 42.817570, display_name = "Orpiri"}, ["Orlovka"] = { latitude = 43.990864, longitude = 43.777097, display_name = "Orlovka"}, ["Orka"] = { latitude = 42.308786, longitude = 42.246602, display_name = "Orka"}, ["Orhvi"] = { latitude = 42.506220, longitude = 42.801449, display_name = "Orhvi"}, ["Orel "] = { latitude = 43.461316, longitude = 39.920078, display_name = "Orel "}, ["Ordzhonikidze"] = { latitude = 42.640714, longitude = 42.025758, display_name = "Ordzhonikidze"}, ["Orbeli"] = { latitude = 42.636184, longitude = 42.822917, display_name = "Orbeli"}, ["Orbel'yanovka"] = { latitude = 44.232740, longitude = 42.878746, display_name = "Orbel'yanovka"}, ["Opshkviti"] = { latitude = 42.147389, longitude = 42.604859, display_name = "Opshkviti"}, ["Opachkhapu"] = { latitude = 42.448393, longitude = 41.903878, display_name = "Opachkhapu"}, ["Ondzhoheti"] = { latitude = 42.032045, longitude = 42.507565, display_name = "Ondzhoheti"}, ["Ol'ginskiy"] = { latitude = 45.126611, longitude = 38.359099, display_name = "Ol'ginskiy"}, ["Okureshi"] = { latitude = 42.543283, longitude = 42.672827, display_name = "Okureshi"}, ["Oktyabr'skoe"] = { latitude = 43.889818, longitude = 43.176085, display_name = "Oktyabr'skoe"}, ["Oktyabr'skiy"] = { latitude = 44.849738, longitude = 38.471831, display_name = "Oktyabr'skiy"}, ["Oktyabr'skiy"] = { latitude = 45.236696, longitude = 38.290841, display_name = "Oktyabr'skiy"}, ["Oktyabr'skiy"] = { latitude = 44.253774, longitude = 42.486886, display_name = "Oktyabr'skiy"}, ["Oktyabr'skiy"] = { latitude = 44.322918, longitude = 39.339558, display_name = "Oktyabr'skiy"}, ["Oktyabr'skaya"] = { latitude = 44.854025, longitude = 39.617850, display_name = "Oktyabr'skaya"}, ["Oktomberi"] = { latitude = 42.208187, longitude = 42.430870, display_name = "Oktomberi"}, ["Oktomberi"] = { latitude = 42.443933, longitude = 41.743717, display_name = "Oktomberi"}, ["Oktomberi"] = { latitude = 43.030969, longitude = 41.229984, display_name = "Oktomberi"}, ["Oireme"] = { latitude = 42.467632, longitude = 41.808728, display_name = "Oireme"}, ["Ohvamekari"] = { latitude = 42.369398, longitude = 41.833464, display_name = "Ohvamekari"}, ["Odzhola"] = { latitude = 42.388595, longitude = 42.785896, display_name = "Odzhola"}, ["Odishi"] = { latitude = 43.075147, longitude = 41.091600, display_name = "Odishi"}, ["Odishi"] = { latitude = 42.526251, longitude = 41.923614, display_name = "Odishi"}, ["Ochkhomuri"] = { latitude = 42.471584, longitude = 42.079849, display_name = "Ochkhomuri"}, ["Ochkhamuri"] = { latitude = 41.856720, longitude = 41.833105, display_name = "Ochkhamuri"}, ["Oche"] = { latitude = 42.524586, longitude = 42.313209, display_name = "Oche"}, ["Obudzhi"] = { latitude = 42.556017, longitude = 42.015172, display_name = "Obudzhi"}, ["Obil'noe"] = { latitude = 44.245620, longitude = 43.557820, display_name = "Obil'noe"}, ["Novyy Sad"] = { latitude = 44.907033, longitude = 38.908686, display_name = "Novyy Sad"}, ["Novyy Karachay "] = { latitude = 43.819752, longitude = 41.904403, display_name = "Novyy Karachay "}, ["Novyy"] = { latitude = 45.007971, longitude = 38.979031, display_name = "Novyy"}, ["Novyy"] = { latitude = 44.151786, longitude = 43.439254, display_name = "Novyy"}, ["Novyy"] = { latitude = 44.935888, longitude = 40.166760, display_name = "Novyy"}, ["Novyy"] = { latitude = 44.726360, longitude = 39.824853, display_name = "Novyy"}, ["Novye Polyany"] = { latitude = 44.302384, longitude = 39.825372, display_name = "Novye Polyany"}, ["Novozavedennoe"] = { latitude = 44.265326, longitude = 43.639520, display_name = "Novozavedennoe"}, ["Novovelichkovskaya"] = { latitude = 45.282561, longitude = 38.846433, display_name = "Novovelichkovskaya"}, ["Novoukrainskiy"] = { latitude = 44.897714, longitude = 38.047940, display_name = "Novoukrainskiy"}, ["Novotroitskiy"] = { latitude = 45.031839, longitude = 38.007409, display_name = "Novotroitskiy"}, ["Novotitarovskaya"] = { latitude = 45.243806, longitude = 38.981038, display_name = "Novotitarovskaya"}, ["Novoterskiy"] = { latitude = 44.150120, longitude = 43.092246, display_name = "Novoterskiy"}, ["Novosrednenskoe"] = { latitude = 44.094686, longitude = 43.823821, display_name = "Novosrednenskoe"}, ["Novosevastopol'skoe"] = { latitude = 45.063035, longitude = 39.704568, display_name = "Novosevastopol'skoe"}, ["Novomyshastovskaya"] = { latitude = 45.201414, longitude = 38.574925, display_name = "Novomyshastovskaya"}, ["Novolabinskaya"] = { latitude = 45.114755, longitude = 39.900432, display_name = "Novolabinskaya"}, ["Novoispravnenskoe"] = { latitude = 43.979804, longitude = 41.542656, display_name = "Novoispravnenskoe"}, ["Novodmitrievskaya"] = { latitude = 44.834141, longitude = 38.877951, display_name = "Novodmitrievskaya"}, ["Novobzhegokay"] = { latitude = 44.934656, longitude = 38.837614, display_name = "Novobzhegokay"}, ["Novoblagodarnoe"] = { latitude = 44.144876, longitude = 42.878900, display_name = "Novoblagodarnoe"}, ["Novoalekseevskoe"] = { latitude = 44.957243, longitude = 39.860670, display_name = "Novoalekseevskoe"}, ["Novaya Akvaskia"] = { latitude = 42.794629, longitude = 41.540276, display_name = "Novaya Akvaskia"}, ["Novaya Adygeya"] = { latitude = 45.028796, longitude = 38.933988, display_name = "Novaya Adygeya"}, ["Nov.Teberda "] = { latitude = 43.673906, longitude = 41.894978, display_name = "Nov.Teberda "}, ["Nov.Dzheguta"] = { latitude = 43.996395, longitude = 42.048760, display_name = "Nov.Dzheguta"}, ["Nosiri"] = { latitude = 42.275005, longitude = 42.139716, display_name = "Nosiri"}, ["Noga"] = { latitude = 42.474952, longitude = 42.208261, display_name = "Noga"}, ["Nizhnyaya Gostagayka"] = { latitude = 45.040575, longitude = 37.349826, display_name = "Nizhnyaya Gostagayka"}, ["Nizhnezol'skiy"] = { latitude = 44.120358, longitude = 43.639111, display_name = "Nizhnezol'skiy"}, ["Nizhnepodkumskiy"] = { latitude = 44.078935, longitude = 43.212710, display_name = "Nizhnepodkumskiy"}, ["Nizhne-Vysokoe"] = { latitude = 43.474346, longitude = 39.972717, display_name = "Nizhne-Vysokoe"}, ["Nizh.Teberda"] = { latitude = 43.638667, longitude = 41.872969, display_name = "Nizh.Teberda"}, ["Nizh.Shilovka"] = { latitude = 43.463994, longitude = 40.023461, display_name = "Nizh.Shilovka"}, ["Nizh.Kurkuzhin"] = { latitude = 43.752549, longitude = 43.362675, display_name = "Nizh.Kurkuzhin"}, ["Nizh.Ermolovka"] = { latitude = 43.750379, longitude = 41.510216, display_name = "Nizh.Ermolovka"}, ["Nizh.Chegem"] = { latitude = 43.498349, longitude = 43.296418, display_name = "Nizh.Chegem"}, ["Nizh.Arhyz"] = { latitude = 43.680448, longitude = 41.458998, display_name = "Nizh.Arhyz"}, ["Nizh. Mtsara "] = { latitude = 43.141038, longitude = 40.761450, display_name = "Nizh. Mtsara "}, ["Nizh. Armyanskoe Uschel'e"] = { latitude = 43.084525, longitude = 40.794689, display_name = "Nizh. Armyanskoe Uschel'e"}, ["Ninoshvili"] = { latitude = 42.042257, longitude = 41.948968, display_name = "Ninoshvili"}, ["Nikortsminda"] = { latitude = 42.464696, longitude = 43.097630, display_name = "Nikortsminda"}, ["Nikolaevskoe"] = { latitude = 44.122613, longitude = 42.129337, display_name = "Nikolaevskoe"}, ["Nikolaenko"] = { latitude = 44.409057, longitude = 39.673508, display_name = "Nikolaenko"}, ["Nigvziani"] = { latitude = 42.067004, longitude = 41.879237, display_name = "Nigvziani"}, ["Nigvzara"] = { latitude = 42.213600, longitude = 43.465632, display_name = "Nigvzara"}, ["Niabauri"] = { latitude = 41.868343, longitude = 42.013388, display_name = "Niabauri"}, ["Nezlobnaya"] = { latitude = 44.118949, longitude = 43.410519, display_name = "Nezlobnaya"}, ["Nezhinskiy"] = { latitude = 43.931930, longitude = 42.685571, display_name = "Nezhinskiy"}, ["Neshukay"] = { latitude = 44.910399, longitude = 39.416240, display_name = "Neshukay"}, ["Nergeeti"] = { latitude = 42.054518, longitude = 42.823643, display_name = "Nergeeti"}, ["Nekrasovskaya"] = { latitude = 45.148939, longitude = 39.755071, display_name = "Nekrasovskaya"}, ["Neftyanaya"] = { latitude = 44.377554, longitude = 39.643419, display_name = "Neftyanaya"}, ["Neftegorsk"] = { latitude = 44.366015, longitude = 39.713219, display_name = "Neftegorsk"}, ["Nebug"] = { latitude = 44.171632, longitude = 38.998327, display_name = "Nebug"}, ["Nebodziri"] = { latitude = 41.988354, longitude = 43.370345, display_name = "Nebodziri"}, ["Neberdzhaevskaya"] = { latitude = 44.828654, longitude = 37.893678, display_name = "Neberdzhaevskaya"}, ["Navenahevi"] = { latitude = 42.250740, longitude = 42.859437, display_name = "Navenahevi"}, ["Navaginka"] = { latitude = 43.618130, longitude = 39.745794, display_name = "Navaginka"}, ["Natuhaevskaya"] = { latitude = 44.911294, longitude = 37.567604, display_name = "Natuhaevskaya"}, ["Natsatu"] = { latitude = 42.567541, longitude = 41.979564, display_name = "Natsatu"}, ["Nasperi"] = { latitude = 42.590925, longitude = 42.770482, display_name = "Nasperi"}, ["Nartan"] = { latitude = 43.511529, longitude = 43.704153, display_name = "Nartan"}, ["Narazeni"] = { latitude = 42.393980, longitude = 41.913391, display_name = "Narazeni"}, ["Naposhtu"] = { latitude = 42.326551, longitude = 41.933522, display_name = "Naposhtu"}, ["Naochi"] = { latitude = 42.776851, longitude = 41.352702, display_name = "Naochi"}, ["Namohvani"] = { latitude = 42.421347, longitude = 42.697503, display_name = "Namohvani"}, ["Namikolavo"] = { latitude = 42.474821, longitude = 42.329612, display_name = "Namikolavo"}, ["Nalepsao"] = { latitude = 42.434450, longitude = 42.365644, display_name = "Nalepsao"}, ["Nakuraleshi"] = { latitude = 42.552601, longitude = 42.746391, display_name = "Nakuraleshi"}, ["Nakipu"] = { latitude = 42.562322, longitude = 42.073330, display_name = "Nakipu"}, ["Nahurtsilavo"] = { latitude = 42.446389, longitude = 42.263325, display_name = "Nahurtsilavo"}, ["Nahunao"] = { latitude = 42.430549, longitude = 42.323061, display_name = "Nahunao"}, ["Nahshirgele"] = { latitude = 42.224265, longitude = 42.824321, display_name = "Nahshirgele"}, ["Nahahulevi"] = { latitude = 42.359411, longitude = 42.438140, display_name = "Nahahulevi"}, ["Naguru"] = { latitude = 42.694093, longitude = 42.097908, display_name = "Naguru"}, ["Nagomari"] = { latitude = 41.990544, longitude = 42.112949, display_name = "Nagomari"}, ["Nageberavo"] = { latitude = 42.332468, longitude = 42.314506, display_name = "Nageberavo"}, ["Naesakao"] = { latitude = 42.178447, longitude = 42.209197, display_name = "Naesakao"}, ["Nadezhnaya"] = { latitude = 44.207001, longitude = 41.404721, display_name = "Nadezhnaya"}, ["Nadaburi"] = { latitude = 42.126553, longitude = 43.427983, display_name = "Nadaburi"}, ["Nabakevi"] = { latitude = 42.503987, longitude = 41.662352, display_name = "Nabakevi"}, ["Mziani"] = { latitude = 41.976759, longitude = 42.151579, display_name = "Mziani"}, ["Myshako"] = { latitude = 44.667140, longitude = 37.759021, display_name = "Myshako"}, ["Muhuri"] = { latitude = 42.634638, longitude = 42.176056, display_name = "Muhuri"}, ["Muhuri"] = { latitude = 42.689556, longitude = 41.697291, display_name = "Muhuri"}, ["Muhura"] = { latitude = 42.326650, longitude = 43.087842, display_name = "Muhura"}, ["Mtsvane-Kontshi"] = { latitude = 41.689072, longitude = 41.709726, display_name = "Mtsvane-Kontshi"}, ["Mtispiri"] = { latitude = 41.941767, longitude = 42.157494, display_name = "Mtispiri"}, ["Morzoh"] = { latitude = 43.567536, longitude = 43.840579, display_name = "Morzoh"}, ["Mongiri"] = { latitude = 42.490006, longitude = 42.152069, display_name = "Mongiri"}, ["Molodezhnyy"] = { latitude = 44.680002, longitude = 39.675713, display_name = "Molodezhnyy"}, ["Moldovka"] = { latitude = 43.454585, longitude = 39.940029, display_name = "Moldovka"}, ["Moldavanskoe"] = { latitude = 44.946939, longitude = 37.868979, display_name = "Moldavanskoe"}, ["Moidanahe"] = { latitude = 42.545104, longitude = 42.138447, display_name = "Moidanahe"}, ["Mohva"] = { latitude = 42.406014, longitude = 43.331100, display_name = "Mohva"}, ["Mohashi"] = { latitude = 42.415311, longitude = 42.190616, display_name = "Mohashi"}, ["Mogukorovskiy"] = { latitude = 45.156215, longitude = 38.198283, display_name = "Mogukorovskiy"}, ["Mogiri"] = { latitude = 42.374700, longitude = 41.729160, display_name = "Mogiri"}, ["Modzvi"] = { latitude = 42.262543, longitude = 43.423399, display_name = "Modzvi"}, ["Mitsatsiteli"] = { latitude = 42.237341, longitude = 42.538850, display_name = "Mitsatsiteli"}, ["Mirnyy"] = { latitude = 44.608512, longitude = 39.008222, display_name = "Mirnyy"}, ["Mingrel'skaya"] = { latitude = 45.014629, longitude = 38.338356, display_name = "Mingrel'skaya"}, ["Mikava"] = { latitude = 42.622078, longitude = 42.105739, display_name = "Mikava"}, ["Mihaylovskoe"] = { latitude = 45.010440, longitude = 38.505502, display_name = "Mihaylovskoe"}, ["Mihaylovskiy Pereval"] = { latitude = 44.515937, longitude = 38.308302, display_name = "Mihaylovskiy Pereval"}, ["Mihaylovka"] = { latitude = 44.220290, longitude = 43.715318, display_name = "Mihaylovka"}, ["Mhkhiani"] = { latitude = 42.198488, longitude = 42.599261, display_name = "Mhkhiani"}, ["Mgvimevi"] = { latitude = 42.324557, longitude = 43.325751, display_name = "Mgvimevi"}, ["Mezmay"] = { latitude = 44.201951, longitude = 39.954704, display_name = "Mezmay"}, ["Messazhay"] = { latitude = 44.141813, longitude = 39.118127, display_name = "Messazhay"}, ["Meria"] = { latitude = 41.944845, longitude = 41.895503, display_name = "Meria"}, ["Merheuli"] = { latitude = 42.987272, longitude = 41.159733, display_name = "Merheuli"}, ["Merdzhevi"] = { latitude = 42.306727, longitude = 43.431058, display_name = "Merdzhevi"}, ["Merchanskoe"] = { latitude = 44.953938, longitude = 38.129010, display_name = "Merchanskoe"}, ["Meore-Tola"] = { latitude = 42.590570, longitude = 43.004131, display_name = "Meore-Tola"}, ["Meore-Sviri"] = { latitude = 42.109908, longitude = 42.927318, display_name = "Meore-Sviri"}, ["Meore-Otobaya"] = { latitude = 42.454627, longitude = 41.646351, display_name = "Meore-Otobaya"}, ["Meore-Obcha"] = { latitude = 42.107171, longitude = 42.887676, display_name = "Meore-Obcha"}, ["Meore-Mohashi"] = { latitude = 42.391827, longitude = 42.155776, display_name = "Meore-Mohashi"}, ["Meore-Gudava"] = { latitude = 42.643386, longitude = 41.544281, display_name = "Meore-Gudava"}, ["Meore-Choga"] = { latitude = 42.553676, longitude = 42.195803, display_name = "Meore-Choga"}, ["Meore-Balda"] = { latitude = 42.499520, longitude = 42.391396, display_name = "Meore-Balda"}, ["Meore Guripuli"] = { latitude = 42.293696, longitude = 41.890921, display_name = "Meore Guripuli"}, ["Melauri"] = { latitude = 42.190643, longitude = 42.384720, display_name = "Melauri"}, ["Medzhinistskali"] = { latitude = 41.591304, longitude = 41.634544, display_name = "Medzhinistskali"}, ["Mednogorskiy"] = { latitude = 43.915956, longitude = 41.186234, display_name = "Mednogorskiy"}, ["Medani"] = { latitude = 42.672074, longitude = 42.137770, display_name = "Medani"}, ["Mechkheturi"] = { latitude = 42.156947, longitude = 43.357366, display_name = "Mechkheturi"}, ["Mazandara"] = { latitude = 42.619401, longitude = 42.045884, display_name = "Mazandara"}, ["Mayskiy"] = { latitude = 44.304185, longitude = 42.414766, display_name = "Mayskiy"}, ["Mathodzhi"] = { latitude = 42.387197, longitude = 42.442078, display_name = "Mathodzhi"}, ["Maruha"] = { latitude = 43.765166, longitude = 41.633133, display_name = "Maruha"}, ["Martotubani"] = { latitude = 42.129811, longitude = 43.099587, display_name = "Martotubani"}, ["Martanskaya"] = { latitude = 44.762337, longitude = 39.437253, display_name = "Martanskaya"}, ["Marelisi"] = { latitude = 41.957941, longitude = 43.275197, display_name = "Marelisi"}, ["Marani"] = { latitude = 42.167287, longitude = 42.279974, display_name = "Marani"}, ["Mar'yanskaya"] = { latitude = 45.105603, longitude = 38.639941, display_name = "Mar'yanskaya"}, ["Mar'inskaya"] = { latitude = 43.883397, longitude = 43.485856, display_name = "Mar'inskaya"}, ["Mar'ina Roscha"] = { latitude = 44.622675, longitude = 38.028581, display_name = "Mar'ina Roscha"}, ["Mandaeti"] = { latitude = 42.183777, longitude = 43.335150, display_name = "Mandaeti"}, ["Mamheg"] = { latitude = 45.014362, longitude = 40.218094, display_name = "Mamheg"}, ["Mamayka"] = { latitude = 43.635005, longitude = 39.702864, display_name = "Mamayka"}, ["Malotenginskaya"] = { latitude = 44.281730, longitude = 41.526014, display_name = "Malotenginskaya"}, ["Malokurgannyy"] = { latitude = 43.844057, longitude = 41.906286, display_name = "Malokurgannyy"}, ["Malka"] = { latitude = 43.800714, longitude = 43.326704, display_name = "Malka"}, ["Mal.Zelenchuk"] = { latitude = 44.160376, longitude = 41.864663, display_name = "Mal.Zelenchuk"}, ["Makopse"] = { latitude = 43.994893, longitude = 39.213327, display_name = "Makopse"}, ["Makatubani"] = { latitude = 42.124781, longitude = 43.243436, display_name = "Makatubani"}, ["Maidani"] = { latitude = 42.284090, longitude = 42.312674, display_name = "Maidani"}, ["Mahatauri"] = { latitude = 42.288011, longitude = 43.463631, display_name = "Mahatauri"}, ["Mahashi"] = { latitude = 42.606742, longitude = 42.749978, display_name = "Mahashi"}, ["Maharadze"] = { latitude = 41.935696, longitude = 41.974045, display_name = "Maharadze"}, ["Maglaki"] = { latitude = 42.261039, longitude = 42.565289, display_name = "Maglaki"}, ["Maevskiy"] = { latitude = 45.172039, longitude = 38.161598, display_name = "Maevskiy"}, ["Machkhvareti"] = { latitude = 42.068987, longitude = 42.008697, display_name = "Machkhvareti"}, ["Lysogorskaya"] = { latitude = 44.106014, longitude = 43.281068, display_name = "Lysogorskaya"}, ["Lunacharskiy"] = { latitude = 43.905269, longitude = 42.690928, display_name = "Lunacharskiy"}, ["Loo"] = { latitude = 43.705439, longitude = 39.587153, display_name = "Loo"}, ["Lineynaya"] = { latitude = 44.594594, longitude = 39.487620, display_name = "Lineynaya"}, ["Liheti"] = { latitude = 42.608896, longitude = 43.238178, display_name = "Liheti"}, ["Lidzava"] = { latitude = 43.178993, longitude = 40.363041, display_name = "Lidzava"}, ["Lia"] = { latitude = 42.636237, longitude = 41.988812, display_name = "Lia"}, ["Levokumka"] = { latitude = 44.234481, longitude = 43.139161, display_name = "Levokumka"}, ["Letsurtsume"] = { latitude = 42.533360, longitude = 42.119200, display_name = "Letsurtsume"}, ["Lesogorskaya"] = { latitude = 44.547683, longitude = 39.536450, display_name = "Lesogorskaya"}, ["Leso-Kefar' "] = { latitude = 43.788871, longitude = 41.449272, display_name = "Leso-Kefar' "}, ["Lesnoe"] = { latitude = 43.776488, longitude = 43.893822, display_name = "Lesnoe"}, ["Leselidze"] = { latitude = 43.394992, longitude = 40.031382, display_name = "Leselidze"}, ["Lesa"] = { latitude = 42.077277, longitude = 41.963379, display_name = "Lesa"}, ["Lepochkhue"] = { latitude = 42.330917, longitude = 42.247525, display_name = "Lepochkhue"}, ["Leninskiy"] = { latitude = 45.172744, longitude = 38.234987, display_name = "Leninskiy"}, ["Leninskiy"] = { latitude = 44.192230, longitude = 43.153106, display_name = "Leninskiy"}, ["Lemikave"] = { latitude = 42.427247, longitude = 42.241238, display_name = "Lemikave"}, ["Lekadzhaie"] = { latitude = 42.405106, longitude = 42.292257, display_name = "Lekadzhaie"}, ["Leharchile"] = { latitude = 42.644704, longitude = 42.136081, display_name = "Leharchile"}, ["Lehaindravo"] = { latitude = 42.354513, longitude = 42.340358, display_name = "Lehaindravo"}, ["Legvani"] = { latitude = 41.967060, longitude = 43.250167, display_name = "Legvani"}, ["Legogie-Nasadzhu"] = { latitude = 42.445929, longitude = 42.182047, display_name = "Legogie-Nasadzhu"}, ["Ledgebe"] = { latitude = 42.486240, longitude = 42.276690, display_name = "Ledgebe"}, ["Ledarsale"] = { latitude = 42.590914, longitude = 42.179511, display_name = "Ledarsale"}, ["Lechkop"] = { latitude = 43.006797, longitude = 40.947857, display_name = "Lechkop"}, ["Lechinkay"] = { latitude = 43.564141, longitude = 43.434332, display_name = "Lechinkay"}, ["Lashkuta"] = { latitude = 43.561224, longitude = 43.220160, display_name = "Lashkuta"}, ["Larchva"] = { latitude = 42.351435, longitude = 41.835304, display_name = "Larchva"}, ["Lailashi"] = { latitude = 42.611170, longitude = 42.859131, display_name = "Lailashi"}, ["L'vovskoe"] = { latitude = 44.996451, longitude = 38.629714, display_name = "L'vovskoe"}, ["Kyzyl-Urup"] = { latitude = 44.007355, longitude = 41.222594, display_name = "Kyzyl-Urup"}, ["Kyzyl-Pokun"] = { latitude = 43.931009, longitude = 42.322452, display_name = "Kyzyl-Pokun"}, ["Kyzyl-Oktyabr'skiy"] = { latitude = 43.821981, longitude = 41.784413, display_name = "Kyzyl-Oktyabr'skiy"}, ["Kyzyl-Kala"] = { latitude = 43.916362, longitude = 42.022165, display_name = "Kyzyl-Kala"}, ["Kyzburun 3-y"] = { latitude = 43.661503, longitude = 43.539209, display_name = "Kyzburun 3-y"}, ["Kyzburun 1-y"] = { latitude = 43.649090, longitude = 43.398417, display_name = "Kyzburun 1-y"}, ["Kvitiri"] = { latitude = 42.240290, longitude = 42.627891, display_name = "Kvitiri"}, ["Kvishona"] = { latitude = 42.483267, longitude = 41.605369, display_name = "Kvishona"}, ["Kvishari"] = { latitude = 42.556177, longitude = 42.944007, display_name = "Kvishari"}, ["Kvilishori"] = { latitude = 42.367595, longitude = 42.633262, display_name = "Kvilishori"}, ["Kvemo-Natanebi"] = { latitude = 41.940818, longitude = 41.821619, display_name = "Kvemo-Natanebi"}, ["Kvemo-Nagvazavo"] = { latitude = 42.369523, longitude = 42.351584, display_name = "Kvemo-Nagvazavo"}, ["Kvemo-Merheuli"] = { latitude = 42.953562, longitude = 41.083145, display_name = "Kvemo-Merheuli"}, ["Kvemo-Makvaneti"] = { latitude = 41.899174, longitude = 42.004806, display_name = "Kvemo-Makvaneti"}, ["Kvemo-Linda"] = { latitude = 43.052983, longitude = 41.084562, display_name = "Kvemo-Linda"}, ["Kvemo-Huntsi"] = { latitude = 42.386495, longitude = 42.408388, display_name = "Kvemo-Huntsi"}, ["Kvemo-Heti"] = { latitude = 42.036326, longitude = 42.345654, display_name = "Kvemo-Heti"}, ["Kvemo-Gumurishi"] = { latitude = 42.695293, longitude = 41.779772, display_name = "Kvemo-Gumurishi"}, ["Kvemo-Chibati"] = { latitude = 42.084840, longitude = 41.984109, display_name = "Kvemo-Chibati"}, ["Kvemo-Bargebi"] = { latitude = 42.550507, longitude = 41.597211, display_name = "Kvemo-Bargebi"}, ["Kvemo-Aketi"] = { latitude = 42.018515, longitude = 42.086106, display_name = "Kvemo-Aketi"}, ["Kvemo-Abasha"] = { latitude = 42.067810, longitude = 42.332917, display_name = "Kvemo-Abasha"}, ["Kvemo-"] = { latitude = 42.534733, longitude = 43.277237, display_name = "Kvemo-"}, ["Kveda-Zegani"] = { latitude = 42.053654, longitude = 42.882304, display_name = "Kveda-Zegani"}, ["Kveda-Tsageri"] = { latitude = 42.636375, longitude = 42.752565, display_name = "Kveda-Tsageri"}, ["Kveda-Tlugi"] = { latitude = 42.450272, longitude = 43.131812, display_name = "Kveda-Tlugi"}, ["Kveda-Simoneti"] = { latitude = 42.224997, longitude = 42.857157, display_name = "Kveda-Simoneti"}, ["Kveda-Mesheti"] = { latitude = 42.212940, longitude = 42.627000, display_name = "Kveda-Mesheti"}, ["Kveda-Kvaliti"] = { latitude = 42.090228, longitude = 42.983547, display_name = "Kveda-Kvaliti"}, ["Kveda-Kinchkha"] = { latitude = 42.487021, longitude = 42.561627, display_name = "Kveda-Kinchkha"}, ["Kveda-Ilemi"] = { latitude = 42.079259, longitude = 43.128506, display_name = "Kveda-Ilemi"}, ["Kveda-Gvirishi"] = { latitude = 42.579735, longitude = 42.800770, display_name = "Kveda-Gvirishi"}, ["Kveda-Gordi"] = { latitude = 42.441570, longitude = 42.521019, display_name = "Kveda-Gordi"}, ["Kveda-Gora"] = { latitude = 42.072625, longitude = 42.664264, display_name = "Kveda-Gora"}, ["Kveda-Chkhorotsku"] = { latitude = 42.494847, longitude = 42.101203, display_name = "Kveda-Chkhorotsku"}, ["Kveda-Chelovani"] = { latitude = 42.361088, longitude = 43.275404, display_name = "Kveda-Chelovani"}, ["Kveda-Bzvani"] = { latitude = 42.076868, longitude = 42.589309, display_name = "Kveda-Bzvani"}, ["Kveda-Bahvi"] = { latitude = 41.961494, longitude = 42.091267, display_name = "Kveda-Bahvi"}, ["Kveda-Alisubani"] = { latitude = 42.241205, longitude = 43.093737, display_name = "Kveda-Alisubani"}, ["Kvatsihe"] = { latitude = 42.291298, longitude = 43.146492, display_name = "Kvatsihe"}, ["Kvashkhieti"] = { latitude = 42.548421, longitude = 43.384416, display_name = "Kvashkhieti"}, ["Kvakude"] = { latitude = 42.054600, longitude = 42.299292, display_name = "Kvakude"}, ["Kvaiti"] = { latitude = 42.425612, longitude = 42.418926, display_name = "Kvaiti"}, ["Kvahchiri"] = { latitude = 42.205749, longitude = 42.738452, display_name = "Kvahchiri"}, ["Kvabga"] = { latitude = 41.923119, longitude = 42.401623, display_name = "Kvabga"}, ["Kuzhorskaya"] = { latitude = 44.670893, longitude = 40.302495, display_name = "Kuzhorskaya"}, ["Kutol"] = { latitude = 42.847172, longitude = 41.384156, display_name = "Kutol"}, ["Kutiri"] = { latitude = 42.266691, longitude = 42.357876, display_name = "Kutiri"}, ["Kutaisskaya"] = { latitude = 44.648472, longitude = 39.310268, display_name = "Kutaisskaya"}, ["Kutais"] = { latitude = 44.526035, longitude = 39.297980, display_name = "Kutais"}, ["Kushubauri"] = { latitude = 42.050474, longitude = 42.439897, display_name = "Kushubauri"}, ["Kurzu"] = { latitude = 42.586949, longitude = 42.289336, display_name = "Kurzu"}, ["Kursebi"] = { latitude = 42.324736, longitude = 42.782359, display_name = "Kursebi"}, ["Kurinskaya"] = { latitude = 44.412581, longitude = 39.419430, display_name = "Kurinskaya"}, ["Kurdzhipskaya"] = { latitude = 44.466941, longitude = 40.052631, display_name = "Kurdzhipskaya"}, ["Kurdzhinovo"] = { latitude = 43.955447, longitude = 40.955449, display_name = "Kurdzhinovo"}, ["Kurchanskaya"] = { latitude = 45.226181, longitude = 37.567587, display_name = "Kurchanskaya"}, ["Kunchukohabl'"] = { latitude = 44.986007, longitude = 39.473929, display_name = "Kunchukohabl'"}, ["Kumysh"] = { latitude = 43.883288, longitude = 41.895281, display_name = "Kumysh"}, ["Kumuri"] = { latitude = 42.035760, longitude = 42.465455, display_name = "Kumuri"}, ["Kumistavi"] = { latitude = 42.385156, longitude = 42.587147, display_name = "Kumistavi"}, ["Kulishkari"] = { latitude = 42.513092, longitude = 41.958707, display_name = "Kulishkari"}, ["Kulevi"] = { latitude = 42.272143, longitude = 41.658712, display_name = "Kulevi"}, ["Kul'tubani"] = { latitude = 43.411775, longitude = 40.018098, display_name = "Kul'tubani"}, ["Kuheshi"] = { latitude = 42.680133, longitude = 42.113696, display_name = "Kuheshi"}, ["Kudepsta"] = { latitude = 43.500558, longitude = 39.893815, display_name = "Kudepsta"}, ["Kuchugury"] = { latitude = 45.408297, longitude = 36.957851, display_name = "Kuchugury"}, ["Kubina"] = { latitude = 44.064788, longitude = 41.946525, display_name = "Kubina"}, ["Kubanskiy"] = { latitude = 44.617741, longitude = 39.773434, display_name = "Kubanskiy"}, ["Kubanskaya"] = { latitude = 44.607766, longitude = 39.710965, display_name = "Kubanskaya"}, ["Kuba-Taba"] = { latitude = 43.781922, longitude = 43.445360, display_name = "Kuba-Taba"}, ["Kuba"] = { latitude = 43.861071, longitude = 43.454167, display_name = "Kuba"}, ["Kroyanskoe"] = { latitude = 44.095332, longitude = 39.120379, display_name = "Kroyanskoe"}, ["Krizhanovskiy"] = { latitude = 45.250930, longitude = 38.164738, display_name = "Krizhanovskiy"}, ["Krivenkovskoe"] = { latitude = 44.192196, longitude = 39.231930, display_name = "Krivenkovskoe"}, ["Krikuna"] = { latitude = 45.267052, longitude = 38.212063, display_name = "Krikuna"}, ["Krepostnaya"] = { latitude = 44.712451, longitude = 38.678714, display_name = "Krepostnaya"}, ["Kremenchug-Konstantinovskiy"] = { latitude = 43.795032, longitude = 43.648364, display_name = "Kremenchug-Konstantinovskiy"}, ["Krasnyy Pahar'"] = { latitude = 44.202575, longitude = 43.104153, display_name = "Krasnyy Pahar'"}, ["Krasnyy Oktyabr'"] = { latitude = 45.195279, longitude = 37.655732, display_name = "Krasnyy Oktyabr'"}, ["Krasnyy Kurgan"] = { latitude = 45.016457, longitude = 37.339404, display_name = "Krasnyy Kurgan"}, ["Krasnyy"] = { latitude = 44.981641, longitude = 38.043088, display_name = "Krasnyy"}, ["Krasnovostochnyy"] = { latitude = 43.967766, longitude = 42.295034, display_name = "Krasnovostochnyy"}, ["Krasnosel'skoe"] = { latitude = 45.292325, longitude = 39.165057, display_name = "Krasnosel'skoe"}, ["Krasnooktyabr'skiy"] = { latitude = 44.932096, longitude = 38.368635, display_name = "Krasnooktyabr'skiy"}, ["Krasnokumskoe"] = { latitude = 44.176715, longitude = 43.484738, display_name = "Krasnokumskoe"}, ["Krasnogvardeyskoe"] = { latitude = 45.124528, longitude = 39.575543, display_name = "Krasnogvardeyskoe"}, ["Krasnogorskaya"] = { latitude = 43.942361, longitude = 41.886798, display_name = "Krasnogorskaya"}, ["Krasnodarskiy"] = { latitude = 45.157592, longitude = 39.135852, display_name = "Krasnodarskiy"}, ["Krasnaya Volya"] = { latitude = 43.562303, longitude = 39.917862, display_name = "Krasnaya Volya"}, ["Krasnaya Batareya"] = { latitude = 45.089560, longitude = 37.786608, display_name = "Krasnaya Batareya"}, ["Krasn.Kurgan"] = { latitude = 43.946104, longitude = 42.607477, display_name = "Krasn.Kurgan"}, ["Kraevsko-Armyanskoe"] = { latitude = 43.607677, longitude = 39.824782, display_name = "Kraevsko-Armyanskoe"}, ["Kozet"] = { latitude = 44.995830, longitude = 38.997301, display_name = "Kozet"}, ["Koydan"] = { latitude = 44.107503, longitude = 42.158336, display_name = "Koydan"}, ["Kosovichi"] = { latitude = 45.090116, longitude = 38.562291, display_name = "Kosovichi"}, ["Kosh-Habl'"] = { latitude = 44.140522, longitude = 41.848038, display_name = "Kosh-Habl'"}, ["Korzhevskiy"] = { latitude = 45.223751, longitude = 38.190642, display_name = "Korzhevskiy"}, ["Korzhevskiy"] = { latitude = 45.196991, longitude = 37.716494, display_name = "Korzhevskiy"}, ["Kortsheli"] = { latitude = 42.561060, longitude = 41.946838, display_name = "Kortsheli"}, ["Koreti"] = { latitude = 42.301668, longitude = 43.380740, display_name = "Koreti"}, ["Korbouli"] = { latitude = 42.239897, longitude = 43.467509, display_name = "Korbouli"}, ["Kopanskiy"] = { latitude = 45.174892, longitude = 38.803876, display_name = "Kopanskiy"}, ["Kontuati"] = { latitude = 42.349493, longitude = 42.418258, display_name = "Kontuati"}, ["Kontianeti"] = { latitude = 42.326672, longitude = 42.166574, display_name = "Kontianeti"}, ["Konstantinovskaya"] = { latitude = 44.049269, longitude = 43.158315, display_name = "Konstantinovskaya"}, ["Konchkati"] = { latitude = 41.983654, longitude = 41.897403, display_name = "Konchkati"}, ["Konchkati"] = { latitude = 42.010778, longitude = 42.018563, display_name = "Konchkati"}, ["Komsomolets"] = { latitude = 44.022420, longitude = 43.562616, display_name = "Komsomolets"}, ["Komsomol'skiy"] = { latitude = 44.936557, longitude = 39.703550, display_name = "Komsomol'skiy"}, ["Kommayak"] = { latitude = 44.110647, longitude = 43.858365, display_name = "Kommayak"}, ["Kolosistyy"] = { latitude = 45.135919, longitude = 38.896495, display_name = "Kolosistyy"}, ["Kolos"] = { latitude = 45.151407, longitude = 38.345172, display_name = "Kolos"}, ["Kolobani"] = { latitude = 42.203259, longitude = 42.267792, display_name = "Kolobani"}, ["Kolhida"] = { latitude = 43.248837, longitude = 40.294920, display_name = "Kolhida"}, ["Koki"] = { latitude = 42.478988, longitude = 41.705012, display_name = "Koki"}, ["Koka"] = { latitude = 42.317730, longitude = 42.829199, display_name = "Koka"}, ["Kochetinskiy"] = { latitude = 45.178268, longitude = 39.238568, display_name = "Kochetinskiy"}, ["Kochara"] = { latitude = 42.853821, longitude = 41.442319, display_name = "Kochara"}, ["Kobuleti"] = { latitude = 41.805916, longitude = 41.884158, display_name = "Kobuleti"}, ["Kobu-Bashi"] = { latitude = 43.932621, longitude = 41.313135, display_name = "Kobu-Bashi"}, ["Kishpek"] = { latitude = 43.649391, longitude = 43.645297, display_name = "Kishpek"}, ["Kirtshi"] = { latitude = 42.483754, longitude = 42.054947, display_name = "Kirtshi"}, ["Kirpichnyy"] = { latitude = 44.041877, longitude = 42.829060, display_name = "Kirpichnyy"}, ["Kirpichnoe"] = { latitude = 44.168178, longitude = 39.201378, display_name = "Kirpichnoe"}, ["Kirov"] = { latitude = 42.415340, longitude = 41.661494, display_name = "Kirov"}, ["Kirilovka"] = { latitude = 44.771935, longitude = 37.724488, display_name = "Kirilovka"}, ["Kievskoe"] = { latitude = 45.039183, longitude = 37.886756, display_name = "Kievskoe"}, ["Kichmalka"] = { latitude = 43.793482, longitude = 42.941509, display_name = "Kichmalka"}, ["Kichi-Balyk"] = { latitude = 43.792728, longitude = 42.650712, display_name = "Kichi-Balyk"}, ["Ketilari"] = { latitude = 42.146402, longitude = 42.169710, display_name = "Ketilari"}, ["Keslerovo"] = { latitude = 45.065249, longitude = 37.820617, display_name = "Keslerovo"}, ["Kerken"] = { latitude = 42.876960, longitude = 41.454944, display_name = "Kerken"}, ["Kemal'pasha"] = { latitude = 41.468809, longitude = 41.519062, display_name = "Kemal'pasha"}, ["Kelermesskaya"] = { latitude = 44.793224, longitude = 40.128338, display_name = "Kelermesskaya"}, ["Kelasuri"] = { latitude = 43.024991, longitude = 41.107288, display_name = "Kelasuri"}, ["Kazazov"] = { latitude = 44.905153, longitude = 39.174224, display_name = "Kazazov"}, ["Kavkazskiy"] = { latitude = 44.269490, longitude = 42.230278, display_name = "Kavkazskiy"}, ["Katshi"] = { latitude = 42.292364, longitude = 43.206406, display_name = "Katshi"}, ["Karskiy"] = { latitude = 44.828921, longitude = 38.509046, display_name = "Karskiy"}, ["Karla Marksa"] = { latitude = 45.239177, longitude = 39.058650, display_name = "Karla Marksa"}, ["Kardonikskaya"] = { latitude = 43.864866, longitude = 41.716166, display_name = "Kardonikskaya"}, ["Karagach"] = { latitude = 43.804645, longitude = 43.776369, display_name = "Karagach"}, ["Kangly"] = { latitude = 44.259543, longitude = 43.029475, display_name = "Kangly"}, ["Kamlyuko"] = { latitude = 43.778866, longitude = 43.260087, display_name = "Kamlyuko"}, ["Kamennomostskoe"] = { latitude = 43.735328, longitude = 43.047845, display_name = "Kamennomostskoe"}, ["Kamennomostskiy"] = { latitude = 43.750261, longitude = 41.907820, display_name = "Kamennomostskiy"}, ["Kaluzhskaya"] = { latitude = 44.763438, longitude = 38.974839, display_name = "Kaluzhskaya"}, ["Kalinovoe Ozero"] = { latitude = 43.615339, longitude = 39.884288, display_name = "Kalinovoe Ozero"}, ["Kalininskiy"] = { latitude = 45.207233, longitude = 40.043834, display_name = "Kalininskiy"}, ["Kalinina"] = { latitude = 44.503982, longitude = 39.720655, display_name = "Kalinina"}, ["Kalezh"] = { latitude = 44.009759, longitude = 39.353566, display_name = "Kalezh"}, ["Kaldahvara"] = { latitude = 43.223294, longitude = 40.418047, display_name = "Kaldahvara"}, ["Kaladzhinskaya"] = { latitude = 44.307522, longitude = 40.905076, display_name = "Kaladzhinskaya"}, ["Kakuti"] = { latitude = 41.859621, longitude = 41.975559, display_name = "Kakuti"}, ["Kahun"] = { latitude = 43.539108, longitude = 43.881699, display_name = "Kahun"}, ["Kahati"] = { latitude = 42.491554, longitude = 41.767683, display_name = "Kahati"}, ["Kachaeti"] = { latitude = 42.475414, longitude = 43.108621, display_name = "Kachaeti"}, ["Kabehabl'"] = { latitude = 45.046603, longitude = 40.180256, display_name = "Kabehabl'"}, ["Kabardinskaya"] = { latitude = 44.501303, longitude = 39.490572, display_name = "Kabardinskaya"}, ["Izumrud"] = { latitude = 43.467734, longitude = 39.956954, display_name = "Izumrud"}, ["Izmaylovka"] = { latitude = 43.630299, longitude = 39.824872, display_name = "Izmaylovka"}, ["Ivanovskaya"] = { latitude = 45.276018, longitude = 38.459475, display_name = "Ivanovskaya"}, ["Ivanov"] = { latitude = 45.114713, longitude = 37.449495, display_name = "Ivanov"}, ["Ithvisi"] = { latitude = 42.299871, longitude = 43.345725, display_name = "Ithvisi"}, ["Isunderi"] = { latitude = 42.556323, longitude = 42.653318, display_name = "Isunderi"}, ["Isula"] = { latitude = 42.234284, longitude = 42.066794, display_name = "Isula"}, ["Ispravnaya"] = { latitude = 44.073144, longitude = 41.608052, display_name = "Ispravnaya"}, ["Islamey"] = { latitude = 43.674212, longitude = 43.451668, display_name = "Islamey"}, ["Inzhich-Chukun"] = { latitude = 44.047223, longitude = 41.782859, display_name = "Inzhich-Chukun"}, ["Inzhi-Chishko"] = { latitude = 44.199712, longitude = 41.716677, display_name = "Inzhi-Chishko"}, ["Intabueti"] = { latitude = 41.974858, longitude = 42.230582, display_name = "Intabueti"}, ["Inkit"] = { latitude = 43.178272, longitude = 40.295540, display_name = "Inkit"}, ["Ingiri"] = { latitude = 42.495609, longitude = 41.813885, display_name = "Ingiri"}, ["Indyuk"] = { latitude = 44.224273, longitude = 39.236887, display_name = "Indyuk"}, ["Industrial'nyy"] = { latitude = 45.097241, longitude = 39.101407, display_name = "Industrial'nyy"}, ["Inchkhuri"] = { latitude = 42.449647, longitude = 42.398849, display_name = "Inchkhuri"}, ["Imeretinskaya"] = { latitude = 44.687871, longitude = 39.425248, display_name = "Imeretinskaya"}, ["im.Tel'mana"] = { latitude = 44.075250, longitude = 42.855761, display_name = "im.Tel'mana"}, ["im.Kosta Hetagurova "] = { latitude = 43.806385, longitude = 41.902590, display_name = "im.Kosta Hetagurova "}, ["Il'ichevskoe"] = { latitude = 44.189752, longitude = 42.146887, display_name = "Il'ichevskoe"}, ["Il'ich"] = { latitude = 45.425374, longitude = 36.769869, display_name = "Il'ich"}, ["Il'ich"] = { latitude = 43.953727, longitude = 41.527379, display_name = "Il'ich"}, ["Ianeuli"] = { latitude = 41.996073, longitude = 42.284336, display_name = "Ianeuli"}, ["Ianeti"] = { latitude = 42.185880, longitude = 42.420261, display_name = "Ianeti"}, ["Hvanchkara"] = { latitude = 42.564505, longitude = 43.017793, display_name = "Hvanchkara"}, ["Hutsubani"] = { latitude = 41.814393, longitude = 41.820341, display_name = "Hutsubani"}, ["Husy-Kardonik"] = { latitude = 43.788087, longitude = 41.572828, display_name = "Husy-Kardonik"}, ["Hushtosyrt"] = { latitude = 43.436177, longitude = 43.236374, display_name = "Hushtosyrt"}, ["Hurzuk"] = { latitude = 43.432373, longitude = 42.149492, display_name = "Hurzuk"}, ["Hunevi"] = { latitude = 42.106239, longitude = 43.362778, display_name = "Hunevi"}, ["Hundzhulouri"] = { latitude = 42.190578, longitude = 42.306336, display_name = "Hundzhulouri"}, ["Humeni-Natopuri"] = { latitude = 42.624068, longitude = 41.611103, display_name = "Humeni-Natopuri"}, ["Humara"] = { latitude = 43.861423, longitude = 41.912994, display_name = "Humara"}, ["Hrialeti"] = { latitude = 42.008259, longitude = 41.820575, display_name = "Hrialeti"}, ["Hresili"] = { latitude = 42.347693, longitude = 42.890246, display_name = "Hresili"}, ["Hreiti"] = { latitude = 42.348511, longitude = 43.178584, display_name = "Hreiti"}, ["Hotevi"] = { latitude = 42.467876, longitude = 43.135786, display_name = "Hotevi"}, ["Horiti"] = { latitude = 42.074583, longitude = 43.212767, display_name = "Horiti"}, ["Honchiori"] = { latitude = 42.498535, longitude = 43.036221, display_name = "Honchiori"}, ["Holodnaya Rechka"] = { latitude = 43.362803, longitude = 40.132876, display_name = "Holodnaya Rechka"}, ["Hole"] = { latitude = 42.629201, longitude = 41.834447, display_name = "Hole"}, ["Hidmagala"] = { latitude = 42.040528, longitude = 41.789749, display_name = "Hidmagala"}, ["Hidistavi"] = { latitude = 41.962817, longitude = 42.192399, display_name = "Hidistavi"}, ["Hidari"] = { latitude = 42.018357, longitude = 43.100231, display_name = "Hidari"}, ["Hevi"] = { latitude = 41.964098, longitude = 42.278006, display_name = "Hevi"}, ["Hetsera"] = { latitude = 42.431467, longitude = 41.879320, display_name = "Hetsera"}, ["Heledi"] = { latitude = 42.794395, longitude = 42.640247, display_name = "Heledi"}, ["Hatukay"] = { latitude = 45.193082, longitude = 39.661811, display_name = "Hatukay"}, ["Hatazhukay"] = { latitude = 45.072561, longitude = 40.182799, display_name = "Hatazhukay"}, ["Hasaut-Grecheskoe "] = { latitude = 43.714868, longitude = 41.668144, display_name = "Hasaut-Grecheskoe "}, ["Hantski"] = { latitude = 42.578465, longitude = 42.222137, display_name = "Hantski"}, ["Hanskaya"] = { latitude = 44.676366, longitude = 39.960442, display_name = "Hanskaya"}, ["Hani"] = { latitude = 41.956643, longitude = 42.957650, display_name = "Hani"}, ["Han'kov"] = { latitude = 45.163180, longitude = 37.866697, display_name = "Han'kov"}, ["Hamyshki"] = { latitude = 44.100538, longitude = 40.129130, display_name = "Hamyshki"}, ["Hamiskuri"] = { latitude = 42.387923, longitude = 41.809094, display_name = "Hamiskuri"}, ["Halipauri"] = { latitude = 42.345266, longitude = 43.305217, display_name = "Halipauri"}, ["Hadzhiko"] = { latitude = 44.008582, longitude = 39.333321, display_name = "Hadzhiko"}, ["Hachemziy"] = { latitude = 44.955574, longitude = 40.318034, display_name = "Hachemziy"}, ["Habez"] = { latitude = 44.042203, longitude = 41.765924, display_name = "Habez"}, ["Habaz"] = { latitude = 43.730189, longitude = 42.931830, display_name = "Habaz"}, ["h.im.Lenina"] = { latitude = 45.024109, longitude = 39.217630, display_name = "h.im.Lenina"}, ["Gyuryul'deuk"] = { latitude = 43.981930, longitude = 42.060924, display_name = "Gyuryul'deuk"}, ["Gvishtibi"] = { latitude = 42.313702, longitude = 42.570687, display_name = "Gvishtibi"}, ["Gvimaroni"] = { latitude = 42.276655, longitude = 41.967510, display_name = "Gvimaroni"}, ["Gverki"] = { latitude = 42.058676, longitude = 43.195791, display_name = "Gverki"}, ["Gvankiti"] = { latitude = 42.168099, longitude = 42.981755, display_name = "Gvankiti"}, ["Gvandra"] = { latitude = 43.046780, longitude = 40.908764, display_name = "Gvandra"}, ["Gurna"] = { latitude = 42.403730, longitude = 42.854106, display_name = "Gurna"}, ["Guriyskaya"] = { latitude = 44.663547, longitude = 39.614414, display_name = "Guriyskaya"}, ["Gurianta"] = { latitude = 41.951175, longitude = 41.931424, display_name = "Gurianta"}, ["Gupagu"] = { latitude = 42.890107, longitude = 41.593118, display_name = "Gupagu"}, ["Gundelen"] = { latitude = 43.597443, longitude = 43.180901, display_name = "Gundelen"}, ["Gundaeti"] = { latitude = 42.241907, longitude = 43.328842, display_name = "Gundaeti"}, ["Gumista"] = { latitude = 43.027157, longitude = 40.945511, display_name = "Gumista"}, ["Gumati"] = { latitude = 42.340711, longitude = 42.678726, display_name = "Gumati"}, ["Guluheti"] = { latitude = 42.240583, longitude = 42.274605, display_name = "Guluheti"}, ["Gubskaya"] = { latitude = 44.317053, longitude = 40.626591, display_name = "Gubskaya"}, ["Gubistskali"] = { latitude = 42.304960, longitude = 42.519440, display_name = "Gubistskali"}, ["Groznyy"] = { latitude = 44.558225, longitude = 40.132270, display_name = "Groznyy"}, ["Grigor'evskaya"] = { latitude = 44.772915, longitude = 38.839703, display_name = "Grigor'evskaya"}, ["Grigolishi"] = { latitude = 42.538524, longitude = 41.972442, display_name = "Grigolishi"}, ["Grigalati"] = { latitude = 42.091739, longitude = 43.397697, display_name = "Grigalati"}, ["Grebeshok"] = { latitude = 43.349031, longitude = 40.158298, display_name = "Grebeshok"}, ["Grazhdanskoe"] = { latitude = 44.223626, longitude = 42.765001, display_name = "Grazhdanskoe"}, ["Goyth"] = { latitude = 44.248215, longitude = 39.372450, display_name = "Goyth"}, ["Gostagaevskaya"] = { latitude = 45.022014, longitude = 37.503797, display_name = "Gostagaevskaya"}, ["Gornyy"] = { latitude = 44.284635, longitude = 39.276932, display_name = "Gornyy"}, ["Gornyy"] = { latitude = 43.958726, longitude = 42.858514, display_name = "Gornyy"}, ["Gorgadzeebi"] = { latitude = 41.719152, longitude = 41.779467, display_name = "Gorgadzeebi"}, ["Goresha"] = { latitude = 42.075887, longitude = 43.257362, display_name = "Goresha"}, ["Goraberezhouli"] = { latitude = 42.004796, longitude = 42.210148, display_name = "Goraberezhouli"}, ["Gonio"] = { latitude = 41.563835, longitude = 41.573319, display_name = "Gonio"}, ["Gonebiskari"] = { latitude = 41.902497, longitude = 42.089475, display_name = "Gonebiskari"}, ["Goncharka"] = { latitude = 44.809236, longitude = 39.954964, display_name = "Goncharka"}, ["Gomi"] = { latitude = 41.887530, longitude = 42.105644, display_name = "Gomi"}, ["Gomi"] = { latitude = 42.613091, longitude = 43.534511, display_name = "Gomi"}, ["Golubitskaya"] = { latitude = 45.325805, longitude = 37.273542, display_name = "Golubitskaya"}, ["Golubeva Dacha"] = { latitude = 43.985387, longitude = 39.231989, display_name = "Golubeva Dacha"}, ["Golovinka"] = { latitude = 43.800018, longitude = 39.460479, display_name = "Golovinka"}, ["Golaskuri"] = { latitude = 42.237078, longitude = 41.979639, display_name = "Golaskuri"}, ["Gogni"] = { latitude = 42.277496, longitude = 42.987314, display_name = "Gogni"}, ["Gofitskoe"] = { latitude = 44.252679, longitude = 40.973461, display_name = "Gofitskoe"}, ["Godogani"] = { latitude = 42.260281, longitude = 42.781294, display_name = "Godogani"}, ["Gocha-Dzhihaishi"] = { latitude = 42.259725, longitude = 42.404511, display_name = "Gocha-Dzhihaishi"}, ["Glola"] = { latitude = 42.703835, longitude = 43.646352, display_name = "Glola"}, ["Glebovskoe"] = { latitude = 44.711034, longitude = 37.639464, display_name = "Glebovskoe"}, ["Gimozgondzhili"] = { latitude = 42.261354, longitude = 41.952437, display_name = "Gimozgondzhili"}, ["Gezati"] = { latitude = 42.222157, longitude = 42.259945, display_name = "Gezati"}, ["Gerpegezh"] = { latitude = 43.375311, longitude = 43.654756, display_name = "Gerpegezh"}, ["Germenchik"] = { latitude = 43.588735, longitude = 43.766319, display_name = "Germenchik"}, ["Georgievskoe"] = { latitude = 44.164187, longitude = 39.251783, display_name = "Georgievskoe"}, ["Georgievskaya"] = { latitude = 44.113588, longitude = 43.480279, display_name = "Georgievskaya"}, ["Gelati"] = { latitude = 42.299240, longitude = 42.763434, display_name = "Gelati"}, ["Geguti"] = { latitude = 42.171259, longitude = 42.674274, display_name = "Geguti"}, ["Gedzheti"] = { latitude = 42.300316, longitude = 42.180255, display_name = "Gedzheti"}, ["Gebi"] = { latitude = 42.769923, longitude = 43.506725, display_name = "Gebi"}, ["Gay-Kodzor"] = { latitude = 44.855566, longitude = 37.436302, display_name = "Gay-Kodzor"}, ["Gaverdovskiy"] = { latitude = 44.615979, longitude = 40.022495, display_name = "Gaverdovskiy"}, ["Gautskinari"] = { latitude = 42.128638, longitude = 42.272212, display_name = "Gautskinari"}, ["Gatlukay"] = { latitude = 44.893152, longitude = 39.232372, display_name = "Gatlukay"}, ["Garkusha"] = { latitude = 45.321862, longitude = 36.849719, display_name = "Garkusha"}, ["Garaha"] = { latitude = 42.517950, longitude = 42.154883, display_name = "Garaha"}, ["Gantiadi"] = { latitude = 42.041995, longitude = 42.389471, display_name = "Gantiadi"}, ["Ganardzhiis-Muhuri"] = { latitude = 42.425698, longitude = 41.627546, display_name = "Ganardzhiis-Muhuri"}, ["Ganahleba"] = { latitude = 42.043143, longitude = 42.199876, display_name = "Ganahleba"}, ["Ganahleba"] = { latitude = 42.929130, longitude = 41.276155, display_name = "Ganahleba"}, ["Gamogma-Shua-Horga"] = { latitude = 42.269829, longitude = 41.795750, display_name = "Gamogma-Shua-Horga"}, ["Gamogma Kariata"] = { latitude = 42.274404, longitude = 41.748423, display_name = "Gamogma Kariata"}, ["Gahomela"] = { latitude = 42.347325, longitude = 42.191623, display_name = "Gahomela"}, ["Gagma-Zanati"] = { latitude = 42.224803, longitude = 42.116445, display_name = "Gagma-Zanati"}, ["Gagma-Shua-Horga"] = { latitude = 42.259303, longitude = 41.816329, display_name = "Gagma-Shua-Horga"}, ["Gagma-Sadzhidzhao"] = { latitude = 42.365068, longitude = 42.038665, display_name = "Gagma-Sadzhidzhao"}, ["Gagma-Dvabzu"] = { latitude = 41.952223, longitude = 42.074319, display_name = "Gagma-Dvabzu"}, ["Gagma-Boslevi"] = { latitude = 42.187975, longitude = 43.175391, display_name = "Gagma-Boslevi"}, ["Fontalovskaya"] = { latitude = 45.365963, longitude = 36.932416, display_name = "Fontalovskaya"}, ["Feria"] = { latitude = 41.632954, longitude = 41.657343, display_name = "Feria"}, ["Fedorovskaya"] = { latitude = 45.080625, longitude = 38.461367, display_name = "Fedorovskaya"}, ["Fazannyy"] = { latitude = 44.039908, longitude = 43.589867, display_name = "Fazannyy"}, ["Fadeevskiy"] = { latitude = 44.642871, longitude = 39.866300, display_name = "Fadeevskiy"}, ["Fadeevo"] = { latitude = 45.068450, longitude = 37.549202, display_name = "Fadeevo"}, ["Ezhedughabl'"] = { latitude = 44.978179, longitude = 39.704512, display_name = "Ezhedughabl'"}, ["Evseevskiy"] = { latitude = 45.055579, longitude = 38.136915, display_name = "Evseevskiy"}, ["Etseri"] = { latitude = 42.554075, longitude = 42.307231, display_name = "Etseri"}, ["Etseri"] = { latitude = 42.259355, longitude = 43.163529, display_name = "Etseri"}, ["Etseri"] = { latitude = 42.212272, longitude = 42.927395, display_name = "Etseri"}, ["Etoko"] = { latitude = 43.948323, longitude = 43.173746, display_name = "Etoko"}, ["Etoka"] = { latitude = 43.916666, longitude = 43.055886, display_name = "Etoka"}, ["Esto-Sadok"] = { latitude = 43.688264, longitude = 40.257190, display_name = "Esto-Sadok"}, ["Essentukskaya"] = { latitude = 44.029667, longitude = 42.870161, display_name = "Essentukskaya"}, ["Erivanskaya"] = { latitude = 44.727221, longitude = 38.181969, display_name = "Erivanskaya"}, ["Erik"] = { latitude = 44.586302, longitude = 39.697324, display_name = "Erik"}, ["Ergeta"] = { latitude = 42.385459, longitude = 41.676872, display_name = "Ergeta"}, ["Erge"] = { latitude = 41.562487, longitude = 41.695039, display_name = "Erge"}, ["Energetik"] = { latitude = 44.071767, longitude = 43.093184, display_name = "Energetik"}, ["Elizavetinskaya"] = { latitude = 45.047901, longitude = 38.795941, display_name = "Elizavetinskaya"}, ["Elenovskoe"] = { latitude = 45.102689, longitude = 39.704562, display_name = "Elenovskoe"}, ["El'tarkach"] = { latitude = 43.982717, longitude = 42.129497, display_name = "El'tarkach"}, ["El'burgan"] = { latitude = 44.076878, longitude = 41.797260, display_name = "El'burgan"}, ["El'brusskiy"] = { latitude = 43.568403, longitude = 42.134528, display_name = "El'brusskiy"}, ["El'brus"] = { latitude = 43.253522, longitude = 42.644636, display_name = "El'brus"}, ["Ekonomicheskoe"] = { latitude = 44.993663, longitude = 37.940817, display_name = "Ekonomicheskoe"}, ["Ekaterinovskiy"] = { latitude = 45.091212, longitude = 38.487168, display_name = "Ekaterinovskiy"}, ["Dzveli-Senaki"] = { latitude = 42.296082, longitude = 42.129269, display_name = "Dzveli-Senaki"}, ["Dzveli-Hibula"] = { latitude = 42.451272, longitude = 41.939322, display_name = "Dzveli-Hibula"}, ["Dzuluhi"] = { latitude = 42.026843, longitude = 42.617118, display_name = "Dzuluhi"}, ["Dzuknuri"] = { latitude = 42.324671, longitude = 42.884988, display_name = "Dzuknuri"}, ["Dzmuisi"] = { latitude = 42.428631, longitude = 42.914125, display_name = "Dzmuisi"}, ["Dzirovani"] = { latitude = 42.351689, longitude = 42.944612, display_name = "Dzirovani"}, ["Dziridzhumati"] = { latitude = 42.017218, longitude = 41.972763, display_name = "Dziridzhumati"}, ["Dziguta"] = { latitude = 43.006590, longitude = 41.058366, display_name = "Dziguta"}, ["Dziguri"] = { latitude = 42.238451, longitude = 42.148827, display_name = "Dziguri"}, ["Dzhvarisa"] = { latitude = 42.383886, longitude = 42.812210, display_name = "Dzhvarisa"}, ["Dzhurukveti"] = { latitude = 42.076304, longitude = 41.922513, display_name = "Dzhurukveti"}, ["Dzhumiti 2-e"] = { latitude = 42.578580, longitude = 42.134227, display_name = "Dzhumiti 2-e"}, ["Dzhumiti 1-e"] = { latitude = 42.551824, longitude = 42.104516, display_name = "Dzhumiti 1-e"}, ["Dzhumi"] = { latitude = 42.451885, longitude = 41.873382, display_name = "Dzhumi"}, ["Dzholevi"] = { latitude = 42.373371, longitude = 42.248034, display_name = "Dzholevi"}, ["Dzhingirik"] = { latitude = 43.736185, longitude = 41.886758, display_name = "Dzhingirik"}, ["Dzhimostaro"] = { latitude = 42.302029, longitude = 42.707023, display_name = "Dzhimostaro"}, ["Dzhihaskari"] = { latitude = 42.511728, longitude = 42.017798, display_name = "Dzhihaskari"}, ["Dzhiginka"] = { latitude = 45.135915, longitude = 37.340154, display_name = "Dzhiginka"}, ["Dzhidzhihabl'"] = { latitude = 44.951191, longitude = 39.408519, display_name = "Dzhidzhihabl'"}, ["Dzhgydyrhva"] = { latitude = 43.178246, longitude = 40.703367, display_name = "Dzhgydyrhva"}, ["Dzhgerda"] = { latitude = 42.910810, longitude = 41.361978, display_name = "Dzhgerda"}, ["Dzherokay"] = { latitude = 44.990610, longitude = 40.307349, display_name = "Dzherokay"}, ["Dzheguta"] = { latitude = 43.967182, longitude = 42.043303, display_name = "Dzheguta"}, ["Dzhapshakari"] = { latitude = 42.409636, longitude = 41.979744, display_name = "Dzhapshakari"}, ["Dzhapana"] = { latitude = 42.095931, longitude = 42.193642, display_name = "Dzhapana"}, ["Dzhambichi"] = { latitude = 45.089652, longitude = 39.854614, display_name = "Dzhambichi"}, ["Dzhalaurta"] = { latitude = 42.248976, longitude = 43.388858, display_name = "Dzhalaurta"}, ["Dzhahunderi"] = { latitude = 42.799916, longitude = 43.022380, display_name = "Dzhahunderi"}, ["Dzhagira"] = { latitude = 42.539297, longitude = 42.056917, display_name = "Dzhagira"}, ["Dzhaga"] = { latitude = 43.954443, longitude = 42.558665, display_name = "Dzhaga"}, ["Dzedzileti"] = { latitude = 42.422529, longitude = 42.560392, display_name = "Dzedzileti"}, ["Dutshuni"] = { latitude = 42.014905, longitude = 42.472516, display_name = "Dutshuni"}, ["Durgena"] = { latitude = 42.240344, longitude = 41.939394, display_name = "Durgena"}, ["Dukmasov"] = { latitude = 45.007570, longitude = 39.914292, display_name = "Dukmasov"}, ["Druzhnyy"] = { latitude = 44.733770, longitude = 39.770438, display_name = "Druzhnyy"}, ["Druzhelyubnyy"] = { latitude = 45.128403, longitude = 39.157197, display_name = "Druzhelyubnyy"}, ["Druzhba"] = { latitude = 44.202308, longitude = 42.015159, display_name = "Druzhba"}, ["Doshake"] = { latitude = 42.522922, longitude = 42.247888, display_name = "Doshake"}, ["Dolina"] = { latitude = 44.237984, longitude = 42.933208, display_name = "Dolina"}, ["Dolgogusevskiy"] = { latitude = 44.813826, longitude = 39.783494, display_name = "Dolgogusevskiy"}, ["Dinskaya"] = { latitude = 45.216067, longitude = 39.228527, display_name = "Dinskaya"}, ["Dimi"] = { latitude = 42.103108, longitude = 42.814050, display_name = "Dimi"}, ["Dilikauri"] = { latitude = 42.156207, longitude = 43.097258, display_name = "Dilikauri"}, ["Dihazurga"] = { latitude = 42.609395, longitude = 41.858275, display_name = "Dihazurga"}, ["Dihashkho"] = { latitude = 42.070267, longitude = 42.560686, display_name = "Dihashkho"}, ["Didvela"] = { latitude = 42.128678, longitude = 42.773856, display_name = "Didvela"}, ["Didi-Opeti"] = { latitude = 42.067712, longitude = 42.370916, display_name = "Didi-Opeti"}, ["Didi-Nedzis-Kahati"] = { latitude = 42.426746, longitude = 41.725656, display_name = "Didi-Nedzis-Kahati"}, ["Didi-Nedzi"] = { latitude = 42.401566, longitude = 41.698956, display_name = "Didi-Nedzi"}, ["Didi-Kuhi"] = { latitude = 42.292505, longitude = 42.452544, display_name = "Didi-Kuhi"}, ["Didi-Horshi"] = { latitude = 42.341781, longitude = 42.084974, display_name = "Didi-Horshi"}, ["Didi-Gantiadi"] = { latitude = 42.097633, longitude = 43.182894, display_name = "Didi-Gantiadi"}, ["Didi-Dzhihaishi"] = { latitude = 42.235623, longitude = 42.438275, display_name = "Didi-Dzhihaishi"}, ["Didi-Chkoni"] = { latitude = 42.494580, longitude = 42.312053, display_name = "Didi-Chkoni"}, ["Dgvaba"] = { latitude = 42.345038, longitude = 41.733485, display_name = "Dgvaba"}, ["Dgnorisa"] = { latitude = 42.469657, longitude = 42.815037, display_name = "Dgnorisa"}, ["Derchi"] = { latitude = 42.468882, longitude = 42.773539, display_name = "Derchi"}, ["Derbentskaya"] = { latitude = 44.768096, longitude = 38.498924, display_name = "Derbentskaya"}, ["Deisi"] = { latitude = 41.967611, longitude = 43.351327, display_name = "Deisi"}, ["Dehviri"] = { latitude = 42.624887, longitude = 42.770402, display_name = "Dehviri"}, ["Defanovka"] = { latitude = 44.432955, longitude = 38.780654, display_name = "Defanovka"}, ["Dedalauri"] = { latitude = 42.354735, longitude = 42.515552, display_name = "Dedalauri"}, ["Davitiani"] = { latitude = 42.458197, longitude = 41.780691, display_name = "Davitiani"}, ["Dausuz"] = { latitude = 43.799532, longitude = 41.550776, display_name = "Dausuz"}, ["Darcheli"] = { latitude = 42.440610, longitude = 41.691733, display_name = "Darcheli"}, ["Dapnari"] = { latitude = 42.103547, longitude = 42.334468, display_name = "Dapnari"}, ["Damanka"] = { latitude = 44.971429, longitude = 37.786142, display_name = "Damanka"}, ["Dahovskaya"] = { latitude = 44.231585, longitude = 40.205041, display_name = "Dahovskaya"}, ["Dagestanskaya"] = { latitude = 44.377390, longitude = 40.019213, display_name = "Dagestanskaya"}, ["Dabla-Gomi"] = { latitude = 42.092557, longitude = 42.383587, display_name = "Dabla-Gomi"}, ["Dabadzveli"] = { latitude = 42.326223, longitude = 42.921560, display_name = "Dabadzveli"}, ["Chvele"] = { latitude = 42.681685, longitude = 41.980907, display_name = "Chvele"}, ["Chuneshi"] = { latitude = 42.356359, longitude = 42.566710, display_name = "Chuneshi"}, ["Chukuli"] = { latitude = 42.811275, longitude = 43.016349, display_name = "Chukuli"}, ["Chuburhindzhi"] = { latitude = 42.582487, longitude = 41.806660, display_name = "Chuburhindzhi"}, ["Chorvila"] = { latitude = 42.284002, longitude = 43.416914, display_name = "Chorvila"}, ["Chordzho-Didi"] = { latitude = 42.562477, longitude = 43.053728, display_name = "Chordzho-Didi"}, ["Chognari"] = { latitude = 42.087419, longitude = 42.284283, display_name = "Chognari"}, ["Chognari"] = { latitude = 42.220218, longitude = 42.760049, display_name = "Chognari"}, ["Chochkhati"] = { latitude = 42.034916, longitude = 41.884302, display_name = "Chochkhati"}, ["Chlou"] = { latitude = 42.872872, longitude = 41.493654, display_name = "Chlou"}, ["Chkvishi"] = { latitude = 42.124911, longitude = 42.435224, display_name = "Chkvishi"}, ["Chkvaleri"] = { latitude = 42.719689, longitude = 42.088272, display_name = "Chkvaleri"}, ["Chkonagora"] = { latitude = 42.088123, longitude = 42.140278, display_name = "Chkonagora"}, ["Chkhuteli"] = { latitude = 42.649842, longitude = 42.789429, display_name = "Chkhuteli"}, ["Chkhoria"] = { latitude = 42.605132, longitude = 41.954171, display_name = "Chkhoria"}, ["Chkhenishi"] = { latitude = 42.228963, longitude = 42.314718, display_name = "Chkhenishi"}, ["Chkaduashi"] = { latitude = 42.599065, longitude = 42.017694, display_name = "Chkaduashi"}, ["Chitatskari"] = { latitude = 42.472242, longitude = 41.852826, display_name = "Chitatskari"}, ["Chiora"] = { latitude = 42.746269, longitude = 43.553631, display_name = "Chiora"}, ["Chihu"] = { latitude = 42.325806, longitude = 41.889458, display_name = "Chihu"}, ["Chiha"] = { latitude = 42.348364, longitude = 43.444360, display_name = "Chiha"}, ["Chernyshov"] = { latitude = 45.053069, longitude = 40.037395, display_name = "Chernyshov"}, ["Chernomorskaya"] = { latitude = 44.702545, longitude = 39.359728, display_name = "Chernomorskaya"}, ["Chernigovskoe"] = { latitude = 44.255092, longitude = 39.760512, display_name = "Chernigovskoe"}, ["Chernigovskaya"] = { latitude = 44.703771, longitude = 39.668488, display_name = "Chernigovskaya"}, ["Chernigovka"] = { latitude = 43.014930, longitude = 41.164718, display_name = "Chernigovka"}, ["Chernaya Rechka"] = { latitude = 43.609516, longitude = 43.836128, display_name = "Chernaya Rechka"}, ["Chereshnya"] = { latitude = 43.444920, longitude = 39.980335, display_name = "Chereshnya"}, ["Chemitokvadze"] = { latitude = 43.839550, longitude = 39.417020, display_name = "Chemitokvadze"}, ["Chemburka"] = { latitude = 44.931292, longitude = 37.340168, display_name = "Chemburka"}, ["Chekon"] = { latitude = 45.109331, longitude = 37.506382, display_name = "Chekon"}, ["Chegem 2-y"] = { latitude = 43.590681, longitude = 43.599375, display_name = "Chegem 2-y"}, ["Chapaevskoe"] = { latitude = 44.286271, longitude = 42.063673, display_name = "Chapaevskoe"}, ["Chalatke"] = { latitude = 42.146661, longitude = 43.025774, display_name = "Chalatke"}, ["Chala"] = { latitude = 41.929410, longitude = 42.048230, display_name = "Chala"}, ["Chakvindzhi"] = { latitude = 42.487273, longitude = 41.979719, display_name = "Chakvindzhi"}, ["Chaisubani"] = { latitude = 41.696688, longitude = 41.781638, display_name = "Chaisubani"}, ["Chagani"] = { latitude = 42.227874, longitude = 42.373545, display_name = "Chagani"}, ["Chagan-Tskvishi"] = { latitude = 42.122079, longitude = 42.415750, display_name = "Chagan-Tskvishi"}, ["Chabanlug"] = { latitude = 43.106712, longitude = 40.797033, display_name = "Chabanlug"}, ["Bzybta 5-y km"] = { latitude = 43.285386, longitude = 40.395535, display_name = "Bzybta 5-y km"}, ["Bzybta 3-y km"] = { latitude = 43.269965, longitude = 40.394210, display_name = "Bzybta 3-y km"}, ["Bzheduhovskaya"] = { latitude = 44.841591, longitude = 39.679353, display_name = "Bzheduhovskaya"}, ["Bynthva"] = { latitude = 43.156061, longitude = 40.739326, display_name = "Bynthva"}, ["BYLYM"] = { latitude = 43.461933, longitude = 43.040023, display_name = "BYLYM"}, ["Bykogorka"] = { latitude = 44.182870, longitude = 42.944364, display_name = "Bykogorka"}, ["Bulitsku"] = { latitude = 42.353732, longitude = 41.883748, display_name = "Bulitsku"}, ["Buknari"] = { latitude = 41.999917, longitude = 42.167892, display_name = "Buknari"}, ["Bratskiy"] = { latitude = 45.233923, longitude = 39.952950, display_name = "Bratskiy"}, ["Bostana"] = { latitude = 42.550059, longitude = 43.075697, display_name = "Bostana"}, ["Borodynovka"] = { latitude = 44.146297, longitude = 43.133294, display_name = "Borodynovka"}, ["Borisovka"] = { latitude = 44.757955, longitude = 37.694273, display_name = "Borisovka"}, ["Bori"] = { latitude = 42.057428, longitude = 43.117675, display_name = "Bori"}, ["Borgustanskaya"] = { latitude = 44.054855, longitude = 42.528751, display_name = "Borgustanskaya"}, ["Bonchkovskiy"] = { latitude = 44.895977, longitude = 38.755977, display_name = "Bonchkovskiy"}, ["Bolgov"] = { latitude = 45.235017, longitude = 39.885843, display_name = "Bolgov"}, ["Bol.Raznokol"] = { latitude = 45.146444, longitude = 37.460326, display_name = "Bol.Raznokol"}, ["Bol'shie Hutora"] = { latitude = 44.747716, longitude = 37.598760, display_name = "Bol'shie Hutora"}, ["Bol'shesidorovskoe"] = { latitude = 45.036635, longitude = 39.838694, display_name = "Bol'shesidorovskoe"}, ["Blagoveschenskaya"] = { latitude = 45.057185, longitude = 37.126383, display_name = "Blagoveschenskaya"}, ["Bia"] = { latitude = 42.344970, longitude = 41.920094, display_name = "Bia"}, ["Bezymyannoe"] = { latitude = 44.553021, longitude = 39.125341, display_name = "Bezymyannoe"}, ["Bezengi"] = { latitude = 43.216814, longitude = 43.286057, display_name = "Bezengi"}, ["Betlemi"] = { latitude = 42.381164, longitude = 42.203732, display_name = "Betlemi"}, ["Besstrashnaya"] = { latitude = 44.253809, longitude = 41.142638, display_name = "Besstrashnaya"}, ["Besleney"] = { latitude = 44.245709, longitude = 41.739140, display_name = "Besleney"}, ["Berezovyy"] = { latitude = 45.150230, longitude = 38.989015, display_name = "Berezovyy"}, ["Belyy Ugol'"] = { latitude = 44.022682, longitude = 42.805498, display_name = "Belyy Ugol'"}, ["Belyy"] = { latitude = 45.170924, longitude = 37.268281, display_name = "Belyy"}, ["Belozernyy"] = { latitude = 45.063400, longitude = 38.674394, display_name = "Belozernyy"}, ["Belokamenskoe"] = { latitude = 43.883528, longitude = 43.022891, display_name = "Belokamenskoe"}, ["Beloe"] = { latitude = 45.051372, longitude = 39.647983, display_name = "Beloe"}, ["Bekeshevskaya"] = { latitude = 44.114902, longitude = 42.433215, display_name = "Bekeshevskaya"}, ["Bazaleti"] = { latitude = 42.034984, longitude = 43.206468, display_name = "Bazaleti"}, ["Bateh"] = { latitude = 43.853210, longitude = 43.231378, display_name = "Bateh"}, ["Bataria"] = { latitude = 42.287102, longitude = 42.014499, display_name = "Bataria"}, ["Bashi"] = { latitude = 42.566591, longitude = 41.926095, display_name = "Bashi"}, ["Bardubani"] = { latitude = 42.210034, longitude = 42.886425, display_name = "Bardubani"}, ["Baranovka"] = { latitude = 43.677734, longitude = 39.708917, display_name = "Baranovka"}, ["Baranikovskiy"] = { latitude = 45.344972, longitude = 38.014303, display_name = "Baranikovskiy"}, ["Banodzha"] = { latitude = 42.287579, longitude = 42.657884, display_name = "Banodzha"}, ["Bandza"] = { latitude = 42.348392, longitude = 42.286405, display_name = "Bandza"}, ["Baksanenok"] = { latitude = 43.687349, longitude = 43.656569, display_name = "Baksanenok"}, ["Bakinskaya"] = { latitude = 44.768913, longitude = 39.281876, display_name = "Bakinskaya"}, ["Bagmarani"] = { latitude = 42.470354, longitude = 41.960753, display_name = "Bagmarani"}, ["Baglan"] = { latitude = 42.830993, longitude = 41.177605, display_name = "Baglan"}, ["Bagikyta"] = { latitude = 43.128318, longitude = 40.696648, display_name = "Bagikyta"}, ["Babugent"] = { latitude = 43.275353, longitude = 43.545330, display_name = "Babugent"}, ["Azovskaya"] = { latitude = 44.793619, longitude = 38.616989, display_name = "Azovskaya"}, ["Aushiger"] = { latitude = 43.395553, longitude = 43.735509, display_name = "Aushiger"}, ["Atydzta"] = { latitude = 43.210846, longitude = 40.392670, display_name = "Atydzta"}, ["Atsydzhkva"] = { latitude = 43.202142, longitude = 40.342933, display_name = "Atsydzhkva"}, ["Assokolay"] = { latitude = 44.845762, longitude = 39.469337, display_name = "Assokolay"}, ["Ashe"] = { latitude = 43.959800, longitude = 39.273025, display_name = "Ashe"}, ["Asfal'tovaya Gora"] = { latitude = 44.463265, longitude = 39.445555, display_name = "Asfal'tovaya Gora"}, ["Armyanskiy"] = { latitude = 44.863314, longitude = 37.993278, display_name = "Armyanskiy"}, ["Arhyz"] = { latitude = 43.565971, longitude = 41.279307, display_name = "Arhyz"}, ["Arhipovskoe"] = { latitude = 45.011892, longitude = 39.852296, display_name = "Arhipovskoe"}, ["Argveta"] = { latitude = 42.142478, longitude = 42.986815, display_name = "Argveta"}, ["Arasadzyh"] = { latitude = 43.233581, longitude = 40.322434, display_name = "Arasadzyh"}, ["Aosyrhva"] = { latitude = 43.198313, longitude = 40.722300, display_name = "Aosyrhva"}, ["Anuhva"] = { latitude = 43.121339, longitude = 40.810995, display_name = "Anuhva"}, ["Anhashtun"] = { latitude = 43.235568, longitude = 40.493729, display_name = "Anhashtun"}, ["Angisa"] = { latitude = 41.631328, longitude = 41.604173, display_name = "Angisa"}, ["Andreevskiy"] = { latitude = 44.228128, longitude = 43.626096, display_name = "Andreevskiy"}, ["Andreevskaya"] = { latitude = 45.320084, longitude = 38.666237, display_name = "Andreevskaya"}, ["Anastasievskaya"] = { latitude = 45.220343, longitude = 37.887568, display_name = "Anastasievskaya"}, ["Anapskaya"] = { latitude = 44.900888, longitude = 37.383960, display_name = "Anapskaya"}, ["Anaklia"] = { latitude = 42.395927, longitude = 41.594732, display_name = "Anaklia"}, ["Amzara"] = { latitude = 43.092069, longitude = 40.991820, display_name = "Amzara"}, ["Amtkel"] = { latitude = 43.036020, longitude = 41.317733, display_name = "Amtkel"}, ["Amsaisi"] = { latitude = 42.135288, longitude = 43.169279, display_name = "Amsaisi"}, ["Amagleba"] = { latitude = 42.085251, longitude = 42.627377, display_name = "Amagleba"}, ["Altud"] = { latitude = 43.719412, longitude = 43.869467, display_name = "Altud"}, ["Alioni"] = { latitude = 42.284723, longitude = 41.949045, display_name = "Alioni"}, ["Ali-Berdukovskiy"] = { latitude = 43.988847, longitude = 41.737681, display_name = "Ali-Berdukovskiy"}, ["Aleksee-Tenginskaya"] = { latitude = 45.210939, longitude = 40.179187, display_name = "Aleksee-Tenginskaya"}, ["Aleksandrovskiy"] = { latitude = 45.255475, longitude = 40.053689, display_name = "Aleksandrovskiy"}, ["Alaverdi"] = { latitude = 42.052523, longitude = 43.065241, display_name = "Alaverdi"}, ["Alambari"] = { latitude = 41.827980, longitude = 41.873672, display_name = "Alambari"}, ["Alahadzy"] = { latitude = 43.221239, longitude = 40.306870, display_name = "Alahadzy"}, ["Al'piyskoe"] = { latitude = 43.290388, longitude = 40.273733, display_name = "Al'piyskoe"}, ["Akvara"] = { latitude = 43.239058, longitude = 40.387451, display_name = "Akvara"}, ["Akvacha"] = { latitude = 43.114639, longitude = 40.833294, display_name = "Akvacha"}, ["Akapa"] = { latitude = 43.037614, longitude = 41.123880, display_name = "Akapa"}, ["Akalamra"] = { latitude = 43.111710, longitude = 40.775246, display_name = "Akalamra"}, ["Ahuti"] = { latitude = 42.471687, longitude = 42.171137, display_name = "Ahuti"}, ["Ahtanizovskaya"] = { latitude = 45.327959, longitude = 37.106970, display_name = "Ahtanizovskaya"}, ["Ahmetovskaya"] = { latitude = 44.151458, longitude = 41.050762, display_name = "Ahmetovskaya"}, ["Ahalsopeli"] = { latitude = 42.251948, longitude = 42.069669, display_name = "Ahalsopeli"}, ["Ahalsopeli"] = { latitude = 42.310765, longitude = 42.949723, display_name = "Ahalsopeli"}, ["Ahalsopeli"] = { latitude = 41.579313, longitude = 41.592645, display_name = "Ahalsopeli"}, ["Ahalsopeli"] = { latitude = 42.166163, longitude = 42.395170, display_name = "Ahalsopeli"}, ["Ahalsopeli"] = { latitude = 42.054131, longitude = 41.841410, display_name = "Ahalsopeli"}, ["Ahalsheni"] = { latitude = 41.971235, longitude = 42.443065, display_name = "Ahalsheni"}, ["Ahalsheni"] = { latitude = 41.624355, longitude = 41.709249, display_name = "Ahalsheni"}, ["Ahalsheni"] = { latitude = 43.118808, longitude = 41.020391, display_name = "Ahalsheni"}, ["Ahalkahati"] = { latitude = 42.468560, longitude = 41.733919, display_name = "Ahalkahati"}, ["Ahali-Terzhola"] = { latitude = 42.232184, longitude = 42.976529, display_name = "Ahali-Terzhola"}, ["Ahali-Sviri"] = { latitude = 42.161011, longitude = 42.903090, display_name = "Ahali-Sviri"}, ["Ahali-Kindgi"] = { latitude = 42.797881, longitude = 41.273547, display_name = "Ahali-Kindgi"}, ["Ahali-Abastumani"] = { latitude = 42.529007, longitude = 41.816390, display_name = "Ahali-Abastumani"}, ["Ahalhibula"] = { latitude = 42.435747, longitude = 42.009298, display_name = "Ahalhibula"}, ["Ahalbediseuli"] = { latitude = 42.382299, longitude = 42.478211, display_name = "Ahalbediseuli"}, ["Agvavera"] = { latitude = 42.733256, longitude = 41.741064, display_name = "Agvavera"}, ["Aguy_Shapsug"] = { latitude = 44.183957, longitude = 39.065643, display_name = "Aguy_Shapsug"}, ["Agronom"] = { latitude = 45.143214, longitude = 39.189885, display_name = "Agronom"}, ["Agoy"] = { latitude = 44.148270, longitude = 39.033715, display_name = "Agoy"}, ["Agaraki"] = { latitude = 43.201028, longitude = 40.412177, display_name = "Agaraki"}, ["Afipsip"] = { latitude = 44.995201, longitude = 38.777506, display_name = "Afipsip"}, ["Adzigezh"] = { latitude = 43.065018, longitude = 40.951904, display_name = "Adzigezh"}, ["Adzhkhahara"] = { latitude = 43.206934, longitude = 40.489845, display_name = "Adzhkhahara"}, ["Adzhazhv"] = { latitude = 42.803694, longitude = 41.475439, display_name = "Adzhazhv"}, ["Adzhapsha"] = { latitude = 43.095728, longitude = 40.735466, display_name = "Adzhapsha"}, ["Adzhameti"] = { latitude = 42.190927, longitude = 42.795745, display_name = "Adzhameti"}, ["Adlia"] = { latitude = 41.616935, longitude = 41.603396, display_name = "Adlia"}, ["Aderbievka"] = { latitude = 44.603960, longitude = 38.106226, display_name = "Aderbievka"}, ["Adamiy"] = { latitude = 45.072339, longitude = 39.495960, display_name = "Adamiy"}, ["Adagum"] = { latitude = 45.095386, longitude = 37.722798, display_name = "Adagum"}, ["Achkvistavi"] = { latitude = 41.829486, longitude = 41.912643, display_name = "Achkvistavi"}, ["Abzhakva"] = { latitude = 43.025609, longitude = 41.068063, display_name = "Abzhakva"}, ["Abgarhuk"] = { latitude = 43.115109, longitude = 40.701987, display_name = "Abgarhuk"}, ["Abedati"] = { latitude = 42.386144, longitude = 42.278763, display_name = "Abedati"}, ["Abastumani"] = { latitude = 42.396615, longitude = 41.876711, display_name = "Abastumani"}, ["Abashispiri"] = { latitude = 42.207775, longitude = 42.166122, display_name = "Abashispiri"}, ["Abanoeti"] = { latitude = 42.539051, longitude = 43.024062, display_name = "Abanoeti"}, ["Abadzehskaya"] = { latitude = 44.393866, longitude = 40.217713, display_name = "Abadzehskaya"}, ["Aatsy"] = { latitude = 43.135637, longitude = 40.731036, display_name = "Aatsy"}, ["Aualitsa"] = { latitude = 43.171207, longitude = 40.665543, display_name = "Aualitsa"}, ["Mugudzyrhva"] = { latitude = 43.155409, longitude = 40.515015, display_name = "Mugudzyrhva"}, ["Othara"] = { latitude = 43.228966, longitude = 40.531859, display_name = "Othara"}, ["Achkatsa"] = { latitude = 43.147649, longitude = 40.685803, display_name = "Achkatsa"}, ["Tvanaarhu"] = { latitude = 43.197101, longitude = 40.652253, display_name = "Tvanaarhu"}, ["Duripsh"] = { latitude = 43.206697, longitude = 40.624055, display_name = "Duripsh"}, ["Abgara"] = { latitude = 43.190179, longitude = 40.624511, display_name = "Abgara"}, ["Synyrhva"] = { latitude = 43.192922, longitude = 40.552701, display_name = "Synyrhva"}, ["Dzhirhva"] = { latitude = 43.206313, longitude = 40.548323, display_name = "Dzhirhva"}, ["Bgardvany"] = { latitude = 43.205393, longitude = 40.595447, display_name = "Bgardvany"}, ["Arhva"] = { latitude = 43.226315, longitude = 40.572106, display_name = "Arhva"}, ["Garp"] = { latitude = 43.237879, longitude = 40.546746, display_name = "Garp"}, ["Adzhimchigra"] = { latitude = 43.167841, longitude = 40.614085, display_name = "Adzhimchigra"}, ["Adzlagara"] = { latitude = 43.148709, longitude = 40.623547, display_name = "Adzlagara"}, ["Algyt"] = { latitude = 43.127564, longitude = 40.557333, display_name = "Algyt"}, ["Mzahva"] = { latitude = 43.137750, longitude = 40.584290, display_name = "Mzahva"}, ["Ahalsopeli"] = { latitude = 43.141474, longitude = 40.553312, display_name = "Ahalsopeli"}, ["Tushurebi"] = { latitude = 42.122766, longitude = 44.912051, display_name = "Tushurebi"}, ["Aloti"] = { latitude = 42.051855, longitude = 44.950279, display_name = "Aloti"}, ["Kvemo-Chala"] = { latitude = 42.029348, longitude = 44.394200, display_name = "Kvemo-Chala"}, ["Mchadidzhvari"] = { latitude = 42.020135, longitude = 44.596836, display_name = "Mchadidzhvari"}, ["Lamiskana"] = { latitude = 42.013894, longitude = 44.490553, display_name = "Lamiskana"}, ["Igoeti"] = { latitude = 41.991304, longitude = 44.413732, display_name = "Igoeti"}, ["Okami"] = { latitude = 41.983290, longitude = 44.474383, display_name = "Okami"}, ["Ksovrisi"] = { latitude = 41.983403, longitude = 44.525294, display_name = "Ksovrisi"}, ["Magraneti"] = { latitude = 41.933666, longitude = 44.988583, display_name = "Magraneti"}, ["Misaktsieli"] = { latitude = 41.948382, longitude = 44.738270, display_name = "Misaktsieli"}, ["Metekhi"] = { latitude = 41.923040, longitude = 44.340959, display_name = "Metekhi"}, ["Khovle"] = { latitude = 41.895157, longitude = 44.239944, display_name = "Khovle"}, ["Zemo-Khandaki"] = { latitude = 41.901323, longitude = 44.313301, display_name = "Zemo-Khandaki"}, ["Garikula"] = { latitude = 41.881470, longitude = 44.334464, display_name = "Garikula"}, ["Agayani"] = { latitude = 41.913671, longitude = 44.546796, display_name = "Agayani"}, ["Tskhvarichamia"] = { latitude = 41.880083, longitude = 44.913670, display_name = "Tskhvarichamia"}, ["Saguramo"] = { latitude = 41.898619, longitude = 44.760362, display_name = "Saguramo"}, ["Kavtishevi"] = { latitude = 41.856681, longitude = 44.442001, display_name = "Kavtishevi"}, ["Gorovani"] = { latitude = 41.884935, longitude = 44.670882, display_name = "Gorovani"}, ["Gldani"] = { latitude = 41.823116, longitude = 44.825815, display_name = "Gldani"}, ["Dzegvi"] = { latitude = 41.846163, longitude = 44.604396, display_name = "Dzegvi"}, ["Norio"] = { latitude = 41.790500, longitude = 44.979697, display_name = "Norio"}, ["Tabakhmela"] = { latitude = 41.653487, longitude = 44.754861, display_name = "Tabakhmela"}, ["farm Krtsanisi"] = { latitude = 41.615412, longitude = 44.908523, display_name = "farm Krtsanisi"}, ["Gamardzhveba"] = { latitude = 41.651053, longitude = 44.988995, display_name = "Gamardzhveba"}, ["Karadzhalari"] = { latitude = 41.622402, longitude = 44.962092, display_name = "Karadzhalari"}, ["Karatagla"] = { latitude = 41.598333, longitude = 44.978450, display_name = "Karatagla"}, ["Asureti"] = { latitude = 41.593966, longitude = 44.671603, display_name = "Asureti"}, ["Tsintskaro"] = { latitude = 41.541691, longitude = 44.617983, display_name = "Tsintskaro"}, ["Kolagiri"] = { latitude = 41.472666, longitude = 44.715622, display_name = "Kolagiri"}, ["Azizkendi"] = { latitude = 41.421746, longitude = 44.945430, display_name = "Azizkendi"}, ["Didi-Mughanlo"] = { latitude = 41.389660, longitude = 44.957250, display_name = "Didi-Mughanlo"}, ["Kizil-Adzhlo"] = { latitude = 41.480203, longitude = 44.767707, display_name = "Kizil-Adzhlo"}, ["State Farm Samgori"] = { latitude = 41.597409, longitude = 45.028851, display_name = "State Farm Samgori"}, ["Birliki"] = { latitude = 41.487802, longitude = 45.072609, display_name = "Birliki"}, ["Hashmi"] = { latitude = 41.758709, longitude = 45.189443, display_name = "Hashmi"}, ["Jandari"] = { latitude = 41.447936, longitude = 45.168489, display_name = "Jandari"}, ["Nazarlo"] = { latitude = 41.423063, longitude = 45.111234, display_name = "Nazarlo"}, ["Patardzeuli"] = { latitude = 41.744385, longitude = 45.248301, display_name = "Patardzeuli"}, ["Zhinvali"] = { latitude = 42.145771, longitude = 44.772532, display_name = "Zhinvali"}, ["Zhinvali"] = { latitude = 42.109544, longitude = 44.765639, display_name = "Zhinvali"}, ["Tianeti"] = { latitude = 42.109786, longitude = 44.965735, display_name = "Tianeti"}, ["Didi-Lilo"] = { latitude = 41.737000, longitude = 44.964292, display_name = "Didi-Lilo"}, ["Manglisi"] = { latitude = 41.699392, longitude = 44.373647, display_name = "Manglisi"}, ["ZAGES"] = { latitude = 41.825864, longitude = 44.757579, display_name = "ZAGES"}, ["Sioni"] = { latitude = 41.990655, longitude = 45.028461, display_name = "Sioni"}, ["Metekhi"] = { latitude = 41.942381, longitude = 44.339043, display_name = "Metekhi"}, ["settlement workers"] = { latitude = 41.871657, longitude = 44.723509, display_name = "settlement workers"}, ["p.Hramzavodstroya"] = { latitude = 41.671265, longitude = 44.916548, display_name = "p.Hramzavodstroya"}, ["Kiketi"] = { latitude = 41.653426, longitude = 44.650357, display_name = "Kiketi"}, ["Vaziani"] = { latitude = 41.693761, longitude = 45.053892, display_name = "Vaziani"}, ["MTSKHETA"] = { latitude = 41.836870, longitude = 44.696831, display_name = "MTSKHETA"}, ["Dzartsemi"] = { latitude = 42.301195, longitude = 43.966521, display_name = "Dzartsemi"}, ["Dzari"] = { latitude = 42.292148, longitude = 43.872880, display_name = "Dzari"}, ["Zemo-Dodoti"] = { latitude = 42.270493, longitude = 43.888601, display_name = "Zemo-Dodoti"}, ["Khetagurov"] = { latitude = 42.212180, longitude = 43.892906, display_name = "Khetagurov"}, ["Ergneti"] = { latitude = 42.198008, longitude = 43.993198, display_name = "Ergneti"}, ["Zemo-Nikozi"] = { latitude = 42.198215, longitude = 43.959361, display_name = "Zemo-Nikozi"}, ["Avnevi"] = { latitude = 42.194477, longitude = 43.876406, display_name = "Avnevi"}, ["Didmukha"] = { latitude = 42.175856, longitude = 43.880354, display_name = "Didmukha"}, ["Phvenisi"] = { latitude = 42.157559, longitude = 43.992365, display_name = "Phvenisi"}, ["Dirbi"] = { latitude = 42.113711, longitude = 43.874980, display_name = "Dirbi"}, ["Sakasheti"] = { latitude = 42.093926, longitude = 43.970841, display_name = "Sakasheti"}, ["Dzlevidzhvari"] = { latitude = 42.106787, longitude = 43.929563, display_name = "Dzlevidzhvari"}, ["Tsveri"] = { latitude = 42.074543, longitude = 43.886696, display_name = "Tsveri"}, ["Kvemo-Hvedureti"] = { latitude = 42.004504, longitude = 43.933894, display_name = "Kvemo-Hvedureti"}, ["Charebi"] = { latitude = 42.266892, longitude = 44.113843, display_name = "Charebi"}, ["Satihari"] = { latitude = 42.256968, longitude = 44.085042, display_name = "Satihari"}, ["Eredvi"] = { latitude = 42.246823, longitude = 44.035496, display_name = "Eredvi"}, ["Berula"] = { latitude = 42.237767, longitude = 44.024776, display_name = "Berula"}, ["Mereti"] = { latitude = 42.225721, longitude = 44.076519, display_name = "Mereti"}, ["Kvemo-Mahisi"] = { latitude = 42.212282, longitude = 44.229800, display_name = "Kvemo-Mahisi"}, ["Karbi"] = { latitude = 42.199130, longitude = 44.075680, display_name = "Karbi"}, ["Ditsi"] = { latitude = 42.209424, longitude = 44.032437, display_name = "Ditsi"}, ["Megvrekisi"] = { latitude = 42.182147, longitude = 44.003578, display_name = "Megvrekisi"}, ["Brotsleti"] = { latitude = 42.179239, longitude = 44.035211, display_name = "Brotsleti"}, ["Didi-Gromi"] = { latitude = 42.159974, longitude = 44.216156, display_name = "Didi-Gromi"}, ["Goyata"] = { latitude = 42.150563, longitude = 44.160480, display_name = "Goyata"}, ["Plavi"] = { latitude = 42.167888, longitude = 44.110547, display_name = "Plavi"}, ["Tkviavi"] = { latitude = 42.158148, longitude = 44.068454, display_name = "Tkviavi"}, ["Shindisi"] = { latitude = 42.129670, longitude = 44.009758, display_name = "Shindisi"}, ["Marana"] = { latitude = 42.143492, longitude = 44.060515, display_name = "Marana"}, ["Kitsnisi"] = { latitude = 42.128901, longitude = 44.091163, display_name = "Kitsnisi"}, ["Kvemo-Artsevi"] = { latitude = 42.137430, longitude = 44.123166, display_name = "Kvemo-Artsevi"}, ["Medzhudispiri"] = { latitude = 42.130753, longitude = 44.197309, display_name = "Medzhudispiri"}, ["Patara-Medzhvrishevi"] = { latitude = 42.138801, longitude = 44.215694, display_name = "Patara-Medzhvrishevi"}, ["Zerti"] = { latitude = 42.105513, longitude = 44.220610, display_name = "Zerti"}, ["Ahrisi"] = { latitude = 42.113127, longitude = 44.169207, display_name = "Ahrisi"}, ["Satemo"] = { latitude = 42.111461, longitude = 44.109531, display_name = "Satemo"}, ["Dzevera"] = { latitude = 42.118587, longitude = 44.052329, display_name = "Dzevera"}, ["Variani"] = { latitude = 42.076311, longitude = 44.033619, display_name = "Variani"}, ["Kvarhiti"] = { latitude = 42.080103, longitude = 44.194531, display_name = "Kvarhiti"}, ["Zegduleti"] = { latitude = 42.053744, longitude = 44.222579, display_name = "Zegduleti"}, ["Heltubani"] = { latitude = 42.051003, longitude = 44.153117, display_name = "Heltubani"}, ["Arashenda"] = { latitude = 42.055347, longitude = 44.026474, display_name = "Arashenda"}, ["Reha"] = { latitude = 42.037624, longitude = 44.116712, display_name = "Reha"}, ["Sveneti"] = { latitude = 42.027015, longitude = 44.147642, display_name = "Sveneti"}, ["Otarasheni"] = { latitude = 42.013074, longitude = 44.086605, display_name = "Otarasheni"}, ["Kldu"] = { latitude = 41.983296, longitude = 43.856146, display_name = "Kldu"}, ["Vedreba"] = { latitude = 41.985291, longitude = 43.821404, display_name = "Vedreba"}, ["Heoba"] = { latitude = 41.970626, longitude = 43.912736, display_name = "Heoba"}, ["Gvleti"] = { latitude = 41.979952, longitude = 43.971435, display_name = "Gvleti"}, ["Skra"] = { latitude = 41.997101, longitude = 44.011659, display_name = "Skra"}, ["Tinihidi"] = { latitude = 41.992653, longitude = 44.084712, display_name = "Tinihidi"}, ["Kvahvreli"] = { latitude = 41.959105, longitude = 44.218573, display_name = "Kvahvreli"}, ["Hidistavi"] = { latitude = 41.959612, longitude = 44.134644, display_name = "Hidistavi"}, ["Bnavisi"] = { latitude = 41.953044, longitude = 44.063967, display_name = "Bnavisi"}, ["settlement workers"] = { latitude = 42.098091, longitude = 44.177467, display_name = "settlement workers"}, ["Tedeleti"] = { latitude = 42.415927, longitude = 43.608474, display_name = "Tedeleti"}, ["Dzhalabeti"] = { latitude = 42.405402, longitude = 43.651559, display_name = "Dzhalabeti"}, ["Perevi"] = { latitude = 42.371801, longitude = 43.598444, display_name = "Perevi"}, ["Dzhriya"] = { latitude = 42.348672, longitude = 43.582496, display_name = "Dzhriya"}, ["Darka"] = { latitude = 42.332744, longitude = 43.555666, display_name = "Darka"}, ["Didi-Tsihiata"] = { latitude = 42.265642, longitude = 43.777229, display_name = "Didi-Tsihiata"}, ["Kornisi"] = { latitude = 42.273295, longitude = 43.820488, display_name = "Kornisi"}, ["Bekmari"] = { latitude = 42.251183, longitude = 43.815920, display_name = "Bekmari"}, ["Ahalsheni"] = { latitude = 42.229380, longitude = 43.768622, display_name = "Ahalsheni"}, ["Nedlati"] = { latitude = 42.215220, longitude = 43.770585, display_name = "Nedlati"}, ["Samtskaro"] = { latitude = 42.200998, longitude = 43.833958, display_name = "Samtskaro"}, ["Nabakevi"] = { latitude = 42.187277, longitude = 43.770297, display_name = "Nabakevi"}, ["Balta"] = { latitude = 42.181644, longitude = 43.720810, display_name = "Balta"}, ["Khvani"] = { latitude = 42.198434, longitude = 43.533608, display_name = "Khvani"}, ["Chalovani"] = { latitude = 42.179914, longitude = 43.504613, display_name = "Chalovani"}, ["Caleti"] = { latitude = 42.168803, longitude = 43.717894, display_name = "Caleti"}, ["Atotsi"] = { latitude = 42.152690, longitude = 43.747844, display_name = "Atotsi"}, ["Lychee"] = { latitude = 42.155902, longitude = 43.482475, display_name = "Lychee"}, ["Tsagvli"] = { latitude = 42.120081, longitude = 43.698204, display_name = "Tsagvli"}, ["Satsihuri"] = { latitude = 42.118858, longitude = 43.731642, display_name = "Satsihuri"}, ["Bredza"] = { latitude = 42.129362, longitude = 43.745254, display_name = "Bredza"}, ["Abisi"] = { latitude = 42.094465, longitude = 43.763261, display_name = "Abisi"}, ["Ptsa"] = { latitude = 42.086530, longitude = 43.786340, display_name = "Ptsa"}, ["Tkotsa"] = { latitude = 42.093459, longitude = 43.702879, display_name = "Tkotsa"}, ["Tshetisdzhvari"] = { latitude = 42.105024, longitude = 43.651165, display_name = "Tshetisdzhvari"}, ["Shaved"] = { latitude = 42.077063, longitude = 43.610677, display_name = "Shaved"}, ["Didi-Plevi"] = { latitude = 42.073444, longitude = 43.698010, display_name = "Didi-Plevi"}, ["Mohisi"] = { latitude = 42.053041, longitude = 43.769400, display_name = "Mohisi"}, ["Vaca"] = { latitude = 42.043164, longitude = 43.713762, display_name = "Vaca"}, ["Nabahtevi"] = { latitude = 42.060954, longitude = 43.666875, display_name = "Nabahtevi"}, ["Tsotshnara"] = { latitude = 42.050883, longitude = 43.579242, display_name = "Tsotshnara"}, ["Gomi"] = { latitude = 42.020584, longitude = 43.725902, display_name = "Gomi"}, ["Kvishheti"] = { latitude = 41.968277, longitude = 43.501974, display_name = "Kvishheti"}, ["Savanisubani"] = { latitude = 41.987763, longitude = 43.524168, display_name = "Savanisubani"}, ["Htsisi"] = { latitude = 41.982473, longitude = 43.675254, display_name = "Htsisi"}, ["Gverdzineti"] = { latitude = 41.934464, longitude = 43.712739, display_name = "Gverdzineti"}, ["Patara-Keleti"] = { latitude = 41.978368, longitude = 43.747280, display_name = "Patara-Keleti"}, ["Sukaantubani"] = { latitude = 41.966189, longitude = 43.782483, display_name = "Sukaantubani"}, ["Surami"] = { latitude = 42.023579, longitude = 43.551661, display_name = "Surami"}, ["Kvomo-Hvtse"] = { latitude = 42.417978, longitude = 43.956401, display_name = "Kvomo-Hvtse"}, ["Garbani"] = { latitude = 42.607571, longitude = 44.583726, display_name = "Garbani"}, ["Hevsha"] = { latitude = 42.397988, longitude = 44.683859, display_name = "Hevsha"}, ["Chargali"] = { latitude = 42.329329, longitude = 44.920772, display_name = "Chargali"}, ["KAZBEGI"] = { latitude = 42.659479, longitude = 44.641080, display_name = "KAZBEGI"}, ["NIGNIY PASANAURI"] = { latitude = 42.395430, longitude = 44.649680, display_name = "NIGNIY PASANAURI"}, ["PASANAURI"] = { latitude = 42.354728, longitude = 44.689155, display_name = "PASANAURI"}, ["MALIY PASANAURI"] = { latitude = 42.352053, longitude = 44.705106, display_name = "MALIY PASANAURI"}, ["NIGNIY PASANAURI"] = { latitude = 42.327877, longitude = 44.681794, display_name = "NIGNIY PASANAURI"}, ["Pavlodolskaya"] = { latitude = 43.725429, longitude = 44.476820, display_name = "Pavlodolskaya"}, ["Kalininskiy"] = { latitude = 43.728693, longitude = 44.688696, display_name = "Kalininskiy"}, ["Stoderevskaya"] = { latitude = 43.725108, longitude = 44.841465, display_name = "Stoderevskaya"}, ["Kievskoye"] = { latitude = 43.706989, longitude = 44.649667, display_name = "Kievskoye"}, ["Kizlyar"] = { latitude = 43.706249, longitude = 44.597507, display_name = "Kizlyar"}, ["Razdolnoe"] = { latitude = 43.698447, longitude = 44.537709, display_name = "Razdolnoe"}, ["Vinogradnoye"] = { latitude = 43.699684, longitude = 44.492810, display_name = "Vinogradnoye"}, ["Novoosetinskaya"] = { latitude = 43.705838, longitude = 44.391716, display_name = "Novoosetinskaya"}, ["Hamidiye"] = { latitude = 43.675286, longitude = 44.377288, display_name = "Hamidiye"}, ["Suhotskoe"] = { latitude = 43.678340, longitude = 44.440575, display_name = "Suhotskoe"}, ["Bratskoye"] = { latitude = 43.654801, longitude = 44.890888, display_name = "Bratskoye"}, ["Chkalovo"] = { latitude = 43.525346, longitude = 44.861239, display_name = "Chkalovo"}, ["Voznesenskaya"] = { latitude = 43.544554, longitude = 44.749833, display_name = "Voznesenskaya"}, ["Stariy Malgobek"] = { latitude = 43.547550, longitude = 44.576478, display_name = "Stariy Malgobek"}, ["Yugnoye"] = { latitude = 43.517917, longitude = 44.743613, display_name = "Yugnoye"}, ["Noviy Redant"] = { latitude = 43.473166, longitude = 44.812043, display_name = "Noviy Redant"}, ["Hurikau"] = { latitude = 43.457393, longitude = 44.459515, display_name = "Hurikau"}, ["Nigniye Achaluki"] = { latitude = 43.402997, longitude = 44.763903, display_name = "Nigniye Achaluki"}, ["Sredniye Achaluki"] = { latitude = 43.371071, longitude = 44.731465, display_name = "Sredniye Achaluki"}, ["Stariy Bataksyurt"] = { latitude = 43.377826, longitude = 44.539760, display_name = "Stariy Bataksyurt"}, ["Verhniy Kurpie"] = { latitude = 43.481149, longitude = 44.372481, display_name = "Verhniy Kurpie"}, ["Zamankul"] = { latitude = 43.349395, longitude = 44.405192, display_name = "Zamankul"}, ["Coban"] = { latitude = 42.917909, longitude = 44.478096, display_name = "Coban"}, ["Tarskoye"] = { latitude = 42.966945, longitude = 44.776350, display_name = "Tarskoye"}, ["Kardzhin"] = { latitude = 43.275762, longitude = 44.304301, display_name = "Kardzhin"}, ["Darg-Koh"] = { latitude = 43.270288, longitude = 44.363370, display_name = "Darg-Koh"}, ["Brut"] = { latitude = 43.269754, longitude = 44.443816, display_name = "Brut"}, ["Humalag"] = { latitude = 43.241187, longitude = 44.478346, display_name = "Humalag"}, ["Zilga"] = { latitude = 43.239199, longitude = 44.522993, display_name = "Zilga"}, ["Dalakova"] = { latitude = 43.240951, longitude = 44.588774, display_name = "Dalakova"}, ["Fahrn"] = { latitude = 43.181612, longitude = 44.497644, display_name = "Fahrn"}, ["Kirovo"] = { latitude = 43.176702, longitude = 44.404846, display_name = "Kirovo"}, ["Noviy Batakoyurt"] = { latitude = 43.220011, longitude = 44.498692, display_name = "Noviy Batakoyurt"}, ["Kadgaron"] = { latitude = 43.134260, longitude = 44.327729, display_name = "Kadgaron"}, ["Ali-Yurt"] = { latitude = 43.143623, longitude = 44.855216, display_name = "Ali-Yurt"}, ["Galashki"] = { latitude = 43.084641, longitude = 44.986559, display_name = "Galashki"}, ["Dongaron"] = { latitude = 43.108707, longitude = 44.720818, display_name = "Dongaron"}, ["Oktyabrskoe"] = { latitude = 43.053531, longitude = 44.746773, display_name = "Oktyabrskoe"}, ["Arhonskaya"] = { latitude = 43.109215, longitude = 44.514273, display_name = "Arhonskaya"}, ["Nart"] = { latitude = 43.119502, longitude = 44.429664, display_name = "Nart"}, ["Komgaron"] = { latitude = 43.054635, longitude = 44.873359, display_name = "Komgaron"}, ["Mayramadag"] = { latitude = 43.022685, longitude = 44.480014, display_name = "Mayramadag"}, ["Dzuarikau"] = { latitude = 43.021167, longitude = 44.406716, display_name = "Dzuarikau"}, ["Nowaya Sabiba"] = { latitude = 43.042338, longitude = 44.533605, display_name = "Nowaya Sabiba"}, ["Hataldon"] = { latitude = 43.037818, longitude = 44.359631, display_name = "Hataldon"}, ["Terk"] = { latitude = 42.931648, longitude = 44.661428, display_name = "Terk"}, ["Bamut"] = { latitude = 43.153786, longitude = 45.199457, display_name = "Bamut"}, ["KARABULAK"] = { latitude = 43.308244, longitude = 44.902324, display_name = "KARABULAK"}, ["Sett. Chapaeva"] = { latitude = 43.544240, longitude = 44.659527, display_name = "Sett. Chapaeva"}, ["Sett. Sheripova"] = { latitude = 43.542711, longitude = 44.617595, display_name = "Sett. Sheripova"}, ["Barzikau"] = { latitude = 42.840858, longitude = 44.311978, display_name = "Barzikau"}, ["Lats"] = { latitude = 42.830939, longitude = 44.291897, display_name = "Lats"}, ["Maloe Kantyshevo"] = { latitude = 43.215812, longitude = 44.676116, display_name = "Maloe Kantyshevo"}, ["Malie Galashki"] = { latitude = 43.119389, longitude = 44.991516, display_name = "Malie Galashki"}, ["Chermen"] = { latitude = 43.124973, longitude = 44.707396, display_name = "Chermen"}, ["YUGNIY"] = { latitude = 42.965960, longitude = 44.689516, display_name = "YUGNIY"}, ["KARTSA"] = { latitude = 43.044612, longitude = 44.729882, display_name = "KARTSA"}, ["Verhniy Komgaron"] = { latitude = 43.056066, longitude = 44.907189, display_name = "Verhniy Komgaron"}, ["Redant 2nd"] = { latitude = 42.988657, longitude = 44.670222, display_name = "Redant 2nd"}, ["DATCHNOE"] = { latitude = 42.976949, longitude = 44.670218, display_name = "DATCHNOE"}, ["Redant 1st"] = { latitude = 42.961780, longitude = 44.658172, display_name = "Redant 1st"}, ["Maliy Terk"] = { latitude = 42.910962, longitude = 44.641989, display_name = "Maliy Terk"}, ["Novopoltavskoe"] = { latitude = 43.690510, longitude = 43.971403, display_name = "Novopoltavskoe"}, ["Novoivanovskoe"] = { latitude = 43.640512, longitude = 43.957150, display_name = "Novoivanovskoe"}, ["Priblizhnaya"] = { latitude = 43.771987, longitude = 44.123433, display_name = "Priblizhnaya"}, ["Yrogainoe"] = { latitude = 43.702547, longitude = 44.216846, display_name = "Yrogainoe"}, ["Terekskoe"] = { latitude = 43.673364, longitude = 44.297188, display_name = "Terekskoe"}, ["Stavd-Durta"] = { latitude = 43.362683, longitude = 44.056242, display_name = "Stavd-Durta"}, ["Zmeyskaya"] = { latitude = 43.344694, longitude = 44.149100, display_name = "Zmeyskaya"}, ["Arik"] = { latitude = 43.581259, longitude = 44.125132, display_name = "Arik"}, ["Verh.Akbash"] = { latitude = 43.474962, longitude = 44.235607, display_name = "Verh.Akbash"}, ["Planovskoye"] = { latitude = 43.403198, longitude = 44.200263, display_name = "Planovskoye"}, ["Lesken"] = { latitude = 43.277818, longitude = 43.829816, display_name = "Lesken"}, ["Chikola"] = { latitude = 43.188667, longitude = 43.919901, display_name = "Chikola"}, ["Ahsay"] = { latitude = 42.957234, longitude = 43.717210, display_name = "Ahsay"}, ["Galiat"] = { latitude = 42.924405, longitude = 43.849611, display_name = "Galiat"}, ["Verhniy Tsey"] = { latitude = 42.803812, longitude = 43.939140, display_name = "Verhniy Tsey"}, ["Verhniy Zaramag"] = { latitude = 42.699861, longitude = 43.961273, display_name = "Verhniy Zaramag"}, ["Tib"] = { latitude = 42.673957, longitude = 43.909848, display_name = "Tib"}, ["Dur-Dur"] = { latitude = 43.122325, longitude = 44.026365, display_name = "Dur-Dur"}, ["Khora"] = { latitude = 43.083592, longitude = 44.067681, display_name = "Khora"}, ["Hod"] = { latitude = 42.878956, longitude = 44.011978, display_name = "Hod"}, ["Nogkau"] = { latitude = 42.869024, longitude = 44.044631, display_name = "Nogkau"}, ["Gusoyta"] = { latitude = 42.866307, longitude = 44.066896, display_name = "Gusoyta"}, ["Nigniy Unal"] = { latitude = 42.863339, longitude = 44.151339, display_name = "Nigniy Unal"}, ["Chasavali"] = { latitude = 42.527264, longitude = 43.645950, display_name = "Chasavali"}, ["Cobet"] = { latitude = 42.524742, longitude = 43.769973, display_name = "Cobet"}, ["Kasagini"] = { latitude = 42.489340, longitude = 43.728148, display_name = "Kasagini"}, ["Biteta"] = { latitude = 42.470286, longitude = 43.696121, display_name = "Biteta"}, ["Hampalgomi"] = { latitude = 42.456412, longitude = 43.736447, display_name = "Hampalgomi"}, ["Dadikau"] = { latitude = 42.463106, longitude = 43.755227, display_name = "Dadikau"}, ["Ertso"] = { latitude = 42.463195, longitude = 43.778098, display_name = "Ertso"}, ["Muguti"] = { latitude = 42.426175, longitude = 43.931004, display_name = "Muguti"}, ["Kotanto"] = { latitude = 42.435038, longitude = 43.842742, display_name = "Kotanto"}, ["Kvemo-Korsevi"] = { latitude = 42.404016, longitude = 43.870838, display_name = "Kvemo-Korsevi"}, ["Stariye Kvemo-Korsevi"] = { latitude = 42.411866, longitude = 43.873899, display_name = "Stariye Kvemo-Korsevi"}, ["Sakire"] = { latitude = 42.378590, longitude = 43.910177, display_name = "Sakire"}, ["Didi-Gupta"] = { latitude = 42.352565, longitude = 43.902265, display_name = "Didi-Gupta"}, ["Kvemo-Sba"] = { latitude = 42.569275, longitude = 44.168972, display_name = "Kvemo-Sba"}, ["Zemo-Roka"] = { latitude = 42.578612, longitude = 44.119959, display_name = "Zemo-Roka"}, ["Kvemo-Roka"] = { latitude = 42.546843, longitude = 44.115709, display_name = "Kvemo-Roka"}, ["Edisa"] = { latitude = 42.538407, longitude = 44.215896, display_name = "Edisa"}, ["Kvemo-Khoshka"] = { latitude = 42.468209, longitude = 44.057935, display_name = "Kvemo-Khoshka"}, ["Elbakita"] = { latitude = 42.428988, longitude = 44.005024, display_name = "Elbakita"}, ["Tsru"] = { latitude = 42.383653, longitude = 44.022967, display_name = "Tsru"}, ["Shua-Tshviri"] = { latitude = 42.372013, longitude = 44.184655, display_name = "Shua-Tshviri"}, ["Klarsi"] = { latitude = 42.353604, longitude = 44.051562, display_name = "Klarsi"}, ["Tsiara"] = { latitude = 42.353420, longitude = 44.022577, display_name = "Tsiara"}, ["VERHNIY ZGID"] = { latitude = 42.871325, longitude = 43.960613, display_name = "VERHNIY ZGID"}, ["SADON"] = { latitude = 42.852012, longitude = 43.995197, display_name = "SADON"}, ["BURON"] = { latitude = 42.795036, longitude = 44.006793, display_name = "BURON"}, ["MIZUR"] = { latitude = 42.851027, longitude = 44.056755, display_name = "MIZUR"}, ["JAVA"] = { latitude = 42.396920, longitude = 43.926887, display_name = "JAVA"}, ["MAISKIY"] = { latitude = 43.641799, longitude = 44.033974, display_name = "MAISKIY"}, ["Uvarovskoye"] = { latitude = 43.815010, longitude = 44.427222, display_name = "Uvarovskoye"}, ["Inarkiev"] = { latitude = 43.472488, longitude = 44.543341, display_name = "Inarkiev"}, ["Verhniye Achaluki"] = { latitude = 43.350538, longitude = 44.699020, display_name = "Verhniye Achaluki"}, ["Yandyrka"] = { latitude = 43.273597, longitude = 44.916689, display_name = "Yandyrka"}, ["Ekazhevo"] = { latitude = 43.210327, longitude = 44.823799, display_name = "Ekazhevo"}, ["Surkhakhi"] = { latitude = 43.187870, longitude = 44.905782, display_name = "Surkhakhi"}, ["Chermen"] = { latitude = 43.148441, longitude = 44.712624, display_name = "Chermen"}, ["Olginskoe"] = { latitude = 43.161052, longitude = 44.691985, display_name = "Olginskoe"}, ["Sunzha"] = { latitude = 43.057253, longitude = 44.825846, display_name = "Sunzha"}, ["Gizel"] = { latitude = 43.047232, longitude = 44.567211, display_name = "Gizel"}, ["Nesterovskaya"] = { latitude = 43.239607, longitude = 45.059246, display_name = "Nesterovskaya"}, ["MALGOBEK"] = { latitude = 43.518590, longitude = 44.599399, display_name = "MALGOBEK"}, ["Carman"] = { latitude = 43.109120, longitude = 44.115426, display_name = "Carman"}, ["Ursdon"] = { latitude = 43.095719, longitude = 44.085430, display_name = "Ursdon"}, ["DIGORA"] = { latitude = 43.159497, longitude = 44.161233, display_name = "DIGORA"}, ["Dzalisi"] = { latitude = 41.963611, longitude = 44.605056, display_name = "Dzalisi"}, ["Digomi"] = { latitude = 41.770418, longitude = 44.741961, display_name = "Digomi"}, ["Kumisi"] = { latitude = 41.615003, longitude = 44.781197, display_name = "Kumisi"}, ["Algeti"] = { latitude = 41.444696, longitude = 44.905321, display_name = "Algeti"}, ["Tsereteli"] = { latitude = 41.449287, longitude = 44.822309, display_name = "Tsereteli"}, ["Akhali-Samgori"] = { latitude = 41.571700, longitude = 45.074511, display_name = "Akhali-Samgori"}, ["Ulyanovka"] = { latitude = 41.389915, longitude = 45.118498, display_name = "Ulyanovka"}, ["Sadyhly"] = { latitude = 41.374920, longitude = 45.144642, display_name = "Sadyhly"}, ["Tskneti"] = { latitude = 41.691738, longitude = 44.698812, display_name = "Tskneti"}, ["TETRA-Tskaro"] = { latitude = 41.547350, longitude = 44.468209, display_name = "TETRA-Tskaro"}, ["Kurta"] = { latitude = 42.283254, longitude = 43.956290, display_name = "Kurta"}, ["Kekhvi"] = { latitude = 42.307566, longitude = 43.940875, display_name = "Kekhvi"}, ["Russkoye"] = { latitude = 43.835069, longitude = 44.579918, display_name = "Russkoye"}, ["Novogeorgievskoe"] = { latitude = 43.763115, longitude = 44.703801, display_name = "Novogeorgievskoe"}, ["Veselovskoye"] = { latitude = 43.771709, longitude = 44.727163, display_name = "Veselovskoye"}, ["Barsuki"] = { latitude = 43.263814, longitude = 44.810861, display_name = "Barsuki"}, ["Pliyevo"] = { latitude = 43.283551, longitude = 44.840391, display_name = "Pliyevo"}, ["Kantyshevo"] = { latitude = 43.230063, longitude = 44.631288, display_name = "Kantyshevo"}, ["Nogir"] = { latitude = 43.078872, longitude = 44.638090, display_name = "Nogir"}, ["Troickaya"] = { latitude = 43.305363, longitude = 45.010858, display_name = "Troickaya"}, ["ZAVODSKOY"] = { latitude = 43.098096, longitude = 44.654284, display_name = "ZAVODSKOY"}, ["BESLAN"] = { latitude = 43.195476, longitude = 44.531621, display_name = "BESLAN"}, ["Nazran"] = { latitude = 43.226212, longitude = 44.777861, display_name = "Nazran"}, ["Ekaterinogradskaya"] = { latitude = 43.766466, longitude = 44.232838, display_name = "Ekaterinogradskaya"}, ["Elhotovo"] = { latitude = 43.356191, longitude = 44.210087, display_name = "Elhotovo"}, ["TEREK"] = { latitude = 43.482380, longitude = 44.141438, display_name = "TEREK"}, ["ALAGIR"] = { latitude = 43.039424, longitude = 44.220280, display_name = "ALAGIR"}, ["Aleksandrovskaya"] = { latitude = 43.485711, longitude = 44.068899, display_name = "Aleksandrovskaya"}, ["Ikoti"] = { latitude = 42.152918, longitude = 44.495123, display_name = "Ikoti"}, ["Mukhrani"] = { latitude = 41.936298, longitude = 44.575647, display_name = "Mukhrani"}, ["Martkobi"] = { latitude = 41.787685, longitude = 45.020138, display_name = "Martkobi"}, ["Mughanlo"] = { latitude = 41.731105, longitude = 45.160083, display_name = "Mughanlo"}, ["Sartichala"] = { latitude = 41.709697, longitude = 45.171715, display_name = "Sartichala"}, ["Leningori"] = { latitude = 42.131932, longitude = 44.485444, display_name = "Leningori"}, ["Lilo"] = { latitude = 41.681176, longitude = 44.976380, display_name = "Lilo"}, ["Dusheti"] = { latitude = 42.084899, longitude = 44.689336, display_name = "Dusheti"}, ["KASPI"] = { latitude = 41.924404, longitude = 44.425303, display_name = "KASPI"}, ["Gardabani"] = { latitude = 41.461640, longitude = 45.092311, display_name = "Gardabani"}, ["Sagarejo"] = { latitude = 41.728389, longitude = 45.332577, display_name = "Sagarejo"}, ["TSKHINVALI"] = { latitude = 42.230304, longitude = 43.970695, display_name = "TSKHINVALI"}, ["GORI"] = { latitude = 41.983833, longitude = 44.110383, display_name = "GORI"}, ["AGARA"] = { latitude = 42.044088, longitude = 43.826962, display_name = "AGARA"}, ["KARELI"] = { latitude = 42.020888, longitude = 43.892648, display_name = "KARELI"}, ["Ruisi"] = { latitude = 42.035966, longitude = 43.964884, display_name = "Ruisi"}, ["Karaleti"] = { latitude = 42.068062, longitude = 44.091374, display_name = "Karaleti"}, ["MOZDOK"] = { latitude = 43.752121, longitude = 44.640819, display_name = "MOZDOK"}, ["VLADIKAVKAZ"] = { latitude = 43.029636, longitude = 44.679665, display_name = "VLADIKAVKAZ"}, ["PROHLADNIY"] = { latitude = 43.756426, longitude = 44.038869, display_name = "PROHLADNIY"}, ["MAYSKIY"] = { latitude = 43.630769, longitude = 44.066323, display_name = "MAYSKIY"}, ["Kurtat"] = { latitude = 43.071083, longitude = 44.751840, display_name = "Kurtat"}, ["RUSTAVI"] = { latitude = 41.559705, longitude = 44.986424, display_name = "RUSTAVI"}, ["Marneuli"] = { latitude = 41.479596, longitude = 44.808583, display_name = "Marneuli"}, ["TBILISI"] = { latitude = 41.736457, longitude = 44.825608, display_name = "TBILISI"}, ["KHASHURI"] = { latitude = 41.985651, longitude = 43.604230, display_name = "KHASHURI"}, } veafNamedPoints._citiesPersianGulf = { ["Aqr"] = { latitude = 24.810000, longitude = 56.440000, display_name = "Aqr"}, ["Al Hadd"] = { latitude = 24.490000, longitude = 56.590000, display_name = "Al Hadd"}, ["Magan"] = { latitude = 24.420000, longitude = 56.580000, display_name = "Magan"}, ["Lar"] = { latitude = 27.661306, longitude = 54.323267, display_name = "Lar"}, ["Bukha"] = { latitude = 26.140000, longitude = 56.150000, display_name = "Bukha"}, ["Al Khadhrawain"] = { latitude = 24.850000, longitude = 56.400000, display_name = "Al Khadhrawain"}, ["Al Bulaydah"] = { latitude = 24.840000, longitude = 56.410000, display_name = "Al Bulaydah"}, ["Al Wadiyat"] = { latitude = 24.800180, longitude = 56.450043, display_name = "Al Wadiyat"}, ["Dabbagh"] = { latitude = 24.540000, longitude = 56.560000, display_name = "Dabbagh"}, ["Sallan"] = { latitude = 24.394327, longitude = 56.726862, display_name = "Sallan"}, ["Majhal"] = { latitude = 24.470000, longitude = 56.330000, display_name = "Majhal"}, ["An Naqdah"] = { latitude = 24.500000, longitude = 56.590000, display_name = "An Naqdah"}, ["Al Liwa"] = { latitude = 24.540000, longitude = 56.570000, display_name = "Al Liwa"}, ["Lekfayir"] = { latitude = 26.040000, longitude = 56.350000, display_name = "Lekfayir"}, ["Maritime City"] = { latitude = 25.267084, longitude = 55.268159, display_name = "Maritime City"}, ["Shaqu"] = { latitude = 27.236470, longitude = 56.361992, display_name = "Shaqu"}, ["Larak Island"] = { latitude = 26.883481, longitude = 56.388735, display_name = "Larak Island"}, ["A Treef"] = { latitude = 24.370000, longitude = 56.710000, display_name = "A Treef"}, ["Falaj al Qabail"] = { latitude = 24.430000, longitude = 56.610000, display_name = "Falaj al Qabail"}, ["Al Mamzar"] = { latitude = 25.303968, longitude = 55.342519, display_name = "Al Mamzar"}, ["Ghadfan"] = { latitude = 24.470000, longitude = 56.600000, display_name = "Ghadfan"}, ["Al Ghashbah"] = { latitude = 24.390000, longitude = 56.690000, display_name = "Al Ghashbah"}, ["Sohar"] = { latitude = 24.344558, longitude = 56.742477, display_name = "Sohar"}, ["Abu Musa Island"] = { latitude = 25.871497, longitude = 55.031741, display_name = "Abu Musa Island"}, ["Ar Ramlah"] = { latitude = 25.508331, longitude = 55.587911, display_name = "Ar Ramlah"}, ["As Salamah"] = { latitude = 25.493873, longitude = 55.591668, display_name = "As Salamah"}, ["Sayh al Asfal"] = { latitude = 26.079096, longitude = 56.340000, display_name = "Sayh al Asfal"}, ["Al Burayk"] = { latitude = 24.399949, longitude = 56.669999, display_name = "Al Burayk"}, ["Sharyat Ayqal"] = { latitude = 26.081123, longitude = 56.341464, display_name = "Sharyat Ayqal"}, ["Al Afifah"] = { latitude = 24.410000, longitude = 56.700000, display_name = "Al Afifah"}, ["Hillat Jan Muhammad Khan"] = { latitude = 24.619982, longitude = 56.520001, display_name = "Hillat Jan Muhammad Khan"}, ["Sur al Mazari"] = { latitude = 24.639898, longitude = 56.519994, display_name = "Sur al Mazari"}, ["Asrar Bani Sad"] = { latitude = 24.590000, longitude = 56.550000, display_name = "Asrar Bani Sad"}, ["Al Makhamarah"] = { latitude = 24.622574, longitude = 56.523492, display_name = "Al Makhamarah"}, ["Juyom"] = { latitude = 28.252947, longitude = 53.982912, display_name = "Juyom"}, ["Khatam Malahah"] = { latitude = 24.970000, longitude = 56.370000, display_name = "Khatam Malahah"}, ["Dadna"] = { latitude = 25.522473, longitude = 56.357902, display_name = "Dadna"}, ["Niad"] = { latitude = 26.100000, longitude = 56.330000, display_name = "Niad"}, ["Hayr Salam"] = { latitude = 26.080000, longitude = 56.330000, display_name = "Hayr Salam"}, ["Ghubn Hamad"] = { latitude = 26.080000, longitude = 56.290000, display_name = "Ghubn Hamad"}, ["Ar Rawdah"] = { latitude = 25.860000, longitude = 56.300000, display_name = "Ar Rawdah"}, ["Al Dhaid"] = { latitude = 25.284179, longitude = 55.879651, display_name = "Al Dhaid"}, ["Hajiabad"] = { latitude = 28.359072, longitude = 54.420670, display_name = "Hajiabad"}, ["Murbah"] = { latitude = 25.282317, longitude = 56.363613, display_name = "Murbah"}, ["Baharestan"] = { latitude = 26.936736, longitude = 56.259130, display_name = "Baharestan"}, ["Hormoz"] = { latitude = 27.095503, longitude = 56.452739, display_name = "Hormoz"}, ["Greater Tunb"] = { latitude = 26.263735, longitude = 55.304914, display_name = "Greater Tunb"}, ["Humaydah"] = { latitude = 24.571042, longitude = 56.387500, display_name = "Humaydah"}, ["Wab Mubarak"] = { latitude = 26.200000, longitude = 56.220000, display_name = "Wab Mubarak"}, ["Tawj"] = { latitude = 26.179087, longitude = 56.219030, display_name = "Tawj"}, ["Farfarah"] = { latitude = 24.720000, longitude = 56.460000, display_name = "Farfarah"}, ["Humayrah"] = { latitude = 24.690031, longitude = 56.469996, display_name = "Humayrah"}, ["Siri Island"] = { latitude = 25.911203, longitude = 54.529233, display_name = "Siri Island"}, ["Rekab al Shib"] = { latitude = 26.130000, longitude = 56.200000, display_name = "Rekab al Shib"}, ["Harf Ghabi"] = { latitude = 26.230105, longitude = 56.210382, display_name = "Harf Ghabi"}, ["Qarat az Zingi"] = { latitude = 26.138520, longitude = 56.216353, display_name = "Qarat az Zingi"}, ["Chahchekor"] = { latitude = 27.278383, longitude = 56.346864, display_name = "Chahchekor"}, ["Baghoo"] = { latitude = 27.311286, longitude = 56.441230, display_name = "Baghoo"}, ["Falara"] = { latitude = 24.749915, longitude = 56.470030, display_name = "Falara"}, ["Al Shinas"] = { latitude = 24.740188, longitude = 56.467926, display_name = "Al Shinas"}, ["Al Hamiliyah"] = { latitude = 24.729932, longitude = 56.459993, display_name = "Al Hamiliyah"}, ["Al Umani"] = { latitude = 24.759906, longitude = 56.459930, display_name = "Al Umani"}, ["Al Ima"] = { latitude = 25.942633, longitude = 56.422684, display_name = "Al Ima"}, ["Salhad"] = { latitude = 25.860000, longitude = 56.220000, display_name = "Salhad"}, ["Masafi"] = { latitude = 25.300995, longitude = 56.161498, display_name = "Masafi"}, ["Al Maksuriyah"] = { latitude = 25.938574, longitude = 56.421403, display_name = "Al Maksuriyah"}, ["Al Quoz"] = { latitude = 25.169070, longitude = 55.253398, display_name = "Al Quoz"}, ["Dib Dibba"] = { latitude = 26.200000, longitude = 56.260000, display_name = "Dib Dibba"}, ["As Sudiyah"] = { latitude = 24.553249, longitude = 56.152529, display_name = "As Sudiyah"}, ["Jask"] = { latitude = 25.643616, longitude = 57.774564, display_name = "Jask"}, ["Dibba Al-Hisn"] = { latitude = 25.614408, longitude = 56.267410, display_name = "Dibba Al-Hisn"}, ["Waab al Lif"] = { latitude = 26.100000, longitude = 56.150000, display_name = "Waab al Lif"}, ["Ghubrat ar Ras"] = { latitude = 26.160000, longitude = 56.250000, display_name = "Ghubrat ar Ras"}, ["Lahbab"] = { latitude = 25.041542, longitude = 55.592377, display_name = "Lahbab"}, ["Nazwa"] = { latitude = 25.005300, longitude = 55.661728, display_name = "Nazwa"}, ["Al Awir"] = { latitude = 25.170430, longitude = 55.546354, display_name = "Al Awir"}, ["Murqquab"] = { latitude = 24.820809, longitude = 55.586540, display_name = "Murqquab"}, ["Sima"] = { latitude = 26.059458, longitude = 56.310949, display_name = "Sima"}, ["Margham"] = { latitude = 24.899518, longitude = 55.625454, display_name = "Margham"}, ["Salakh"] = { latitude = 26.691055, longitude = 55.708540, display_name = "Salakh"}, ["Ghamtha"] = { latitude = 26.110000, longitude = 56.130000, display_name = "Ghamtha"}, ["Al Usayli"] = { latitude = 25.626760, longitude = 56.008144, display_name = "Al Usayli"}, ["Wad Wid"] = { latitude = 25.625149, longitude = 56.013964, display_name = "Wad Wid"}, ["Al Hillah"] = { latitude = 26.101415, longitude = 56.139522, display_name = "Al Hillah"}, ["Fawhfallam"] = { latitude = 26.109031, longitude = 56.150113, display_name = "Fawhfallam"}, ["Al Khan"] = { latitude = 24.225589, longitude = 56.326442, display_name = "Al Khan"}, ["Shahr-e ghadim"] = { latitude = 27.680462, longitude = 54.339229, display_name = "Shahr-e ghadim"}, ["Shahr-e jadid"] = { latitude = 27.649446, longitude = 54.318973, display_name = "Shahr-e jadid"}, ["Gharbiyah"] = { latitude = 25.610141, longitude = 56.250002, display_name = "Gharbiyah"}, ["Kavarzin"] = { latitude = 26.796507, longitude = 55.831578, display_name = "Kavarzin"}, ["Khonj"] = { latitude = 27.889926, longitude = 53.436782, display_name = "Khonj"}, ["Bayah"] = { latitude = 25.670000, longitude = 56.260000, display_name = "Bayah"}, ["Al Shuwara"] = { latitude = 26.150000, longitude = 56.290000, display_name = "Al Shuwara"}, ["Diba al Bayah"] = { latitude = 25.640977, longitude = 56.267373, display_name = "Diba al Bayah"}, ["Al Karsha"] = { latitude = 25.656732, longitude = 56.267520, display_name = "Al Karsha"}, ["Al Hubayl"] = { latitude = 26.100000, longitude = 56.240000, display_name = "Al Hubayl"}, ["Bur Dubai"] = { latitude = 25.262134, longitude = 55.297289, display_name = "Bur Dubai"}, ["Port Saeed"] = { latitude = 25.245766, longitude = 55.333673, display_name = "Port Saeed"}, ["Gerash"] = { latitude = 27.670884, longitude = 54.139243, display_name = "Gerash"}, ["Al Raffa"] = { latitude = 25.255162, longitude = 55.288470, display_name = "Al Raffa"}, ["Al Mankhool"] = { latitude = 25.250062, longitude = 55.294734, display_name = "Al Mankhool"}, ["Deh-e Now"] = { latitude = 27.347680, longitude = 56.544424, display_name = "Deh-e Now"}, ["Umm Al Quwain"] = { latitude = 25.552043, longitude = 55.547498, display_name = "Umm Al Quwain"}, ["Majaz al Kubra"] = { latitude = 24.240000, longitude = 56.840000, display_name = "Majaz al Kubra"}, ["Simah"] = { latitude = 26.061530, longitude = 56.309655, display_name = "Simah"}, ["Al Garhoud"] = { latitude = 25.241767, longitude = 55.350073, display_name = "Al Garhoud"}, ["Umm Ramool"] = { latitude = 25.232361, longitude = 55.366826, display_name = "Umm Ramool"}, ["Bandar Abbas"] = { latitude = 27.178121, longitude = 56.276645, display_name = "Bandar Abbas"}, ["Dayrestan"] = { latitude = 26.745164, longitude = 55.934918, display_name = "Dayrestan"}, ["Al Madam"] = { latitude = 24.976101, longitude = 55.789605, display_name = "Al Madam"}, ["Abu Musa"] = { latitude = 25.885745, longitude = 55.037629, display_name = "Abu Musa"}, ["Ajman"] = { latitude = 25.393656, longitude = 55.445143, display_name = "Ajman"}, ["Dehbarez"] = { latitude = 27.447061, longitude = 57.190751, display_name = "Dehbarez"}, ["Al Fiduk"] = { latitude = 26.030000, longitude = 56.360000, display_name = "Al Fiduk"}, ["Tomban"] = { latitude = 26.769382, longitude = 55.864493, display_name = "Tomban"}, ["Dargahan"] = { latitude = 26.967400, longitude = 56.078113, display_name = "Dargahan"}, ["Al Wasl"] = { latitude = 25.197181, longitude = 55.254978, display_name = "Al Wasl"}, ["Ziaratali"] = { latitude = 27.739415, longitude = 57.219915, display_name = "Ziaratali"}, ["Muslaf"] = { latitude = 24.700000, longitude = 56.290000, display_name = "Muslaf"}, ["Subakh"] = { latitude = 24.720000, longitude = 56.180000, display_name = "Subakh"}, ["Sur Khusaybi"] = { latitude = 24.660000, longitude = 56.510000, display_name = "Sur Khusaybi"}, ["Birkat Khaldiyah"] = { latitude = 26.040000, longitude = 56.360000, display_name = "Birkat Khaldiyah"}, ["Turayf"] = { latitude = 24.670000, longitude = 56.480000, display_name = "Turayf"}, ["Salib"] = { latitude = 26.369819, longitude = 56.359555, display_name = "Salib"}, ["Al Darai"] = { latitude = 26.150000, longitude = 56.260000, display_name = "Al Darai"}, ["Sharjah"] = { latitude = 25.350866, longitude = 55.384186, display_name = "Sharjah"}, ["Rayy"] = { latitude = 24.650000, longitude = 56.110000, display_name = "Rayy"}, ["Fin"] = { latitude = 27.627471, longitude = 55.902724, display_name = "Fin"}, ["Al Jari"] = { latitude = 26.216764, longitude = 56.185602, display_name = "Al Jari"}, ["Sarriq-e Meyguni"] = { latitude = 27.300313, longitude = 56.276998, display_name = "Sarriq-e Meyguni"}, ["Sawt"] = { latitude = 25.660000, longitude = 56.260000, display_name = "Sawt"}, ["Jenah"] = { latitude = 27.017683, longitude = 54.282941, display_name = "Jenah"}, ["Hashtbandi"] = { latitude = 26.813131, longitude = 57.832783, display_name = "Hashtbandi"}, ["Fiqa"] = { latitude = 24.716533, longitude = 55.621624, display_name = "Fiqa"}, ["Arabian Ranches"] = { latitude = 25.051751, longitude = 55.265756, display_name = "Arabian Ranches"}, ["Mukhaylif"] = { latitude = 24.520000, longitude = 56.570000, display_name = "Mukhaylif"}, ["Umm Suqeim 3"] = { latitude = 25.138722, longitude = 55.195982, display_name = "Umm Suqeim 3"}, ["Al Manara"] = { latitude = 25.145203, longitude = 55.214933, display_name = "Al Manara"}, ["Umm Suqueim 1"] = { latitude = 25.165045, longitude = 55.217017, display_name = "Umm Suqueim 1"}, ["Harat ash Shaykh"] = { latitude = 24.500000, longitude = 56.580000, display_name = "Harat ash Shaykh"}, ["Al Jowar"] = { latitude = 26.070000, longitude = 56.220000, display_name = "Al Jowar"}, ["Bandar-e Lengeh"] = { latitude = 26.557134, longitude = 54.881714, display_name = "Bandar-e Lengeh"}, ["Suza"] = { latitude = 26.777574, longitude = 56.063126, display_name = "Suza"}, ["Al Mabrak"] = { latitude = 26.150000, longitude = 56.310000, display_name = "Al Mabrak"}, ["Seerik"] = { latitude = 26.518435, longitude = 57.104636, display_name = "Seerik"}, ["Bandar Khamir"] = { latitude = 26.951638, longitude = 55.587708, display_name = "Bandar Khamir"}, ["Khasab"] = { latitude = 26.180182, longitude = 56.249618, display_name = "Khasab"}, ["Al Marqadh"] = { latitude = 25.169438, longitude = 55.289759, display_name = "Al Marqadh"}, ["Fujairah"] = { latitude = 25.125387, longitude = 56.343680, display_name = "Fujairah"}, ["Al Masharta"] = { latitude = 26.150000, longitude = 56.280000, display_name = "Al Masharta"}, ["Senderk"] = { latitude = 26.593623, longitude = 57.863963, display_name = "Senderk"}, ["Bikah"] = { latitude = 27.354812, longitude = 57.180416, display_name = "Bikah"}, ["Al Jadi"] = { latitude = 26.160000, longitude = 56.170000, display_name = "Al Jadi"}, ["Fareghan"] = { latitude = 28.007798, longitude = 56.255127, display_name = "Fareghan"}, ["Bastak"] = { latitude = 27.105005, longitude = 54.455254, display_name = "Bastak"}, ["Patil Posht-e Banu Band"] = { latitude = 27.298063, longitude = 56.180439, display_name = "Patil Posht-e Banu Band"}, ["Mobarakabad"] = { latitude = 28.359589, longitude = 53.328186, display_name = "Mobarakabad"}, ["Din ar Rukayb"] = { latitude = 26.050000, longitude = 56.300000, display_name = "Din ar Rukayb"}, ["Evaz"] = { latitude = 27.762532, longitude = 54.005150, display_name = "Evaz"}, ["Sabakh"] = { latitude = 24.527897, longitude = 56.468961, display_name = "Sabakh"}, ["Sahil Harmul"] = { latitude = 24.530000, longitude = 56.600000, display_name = "Sahil Harmul"}, ["Sistan"] = { latitude = 26.940766, longitude = 56.257473, display_name = "Sistan"}, ["Bayt ash Shaykh"] = { latitude = 26.070000, longitude = 56.210000, display_name = "Bayt ash Shaykh"}, ["Kabir"] = { latitude = 25.432204, longitude = 55.705133, display_name = "Kabir"}, ["United Arab Emirates"] = { latitude = 24.871963, longitude = 55.255096, display_name = "United Arab Emirates"}, ["Pa Tall-e Isin"] = { latitude = 27.301953, longitude = 56.239318, display_name = "Pa Tall-e Isin"}, ["Abu Dhabi"] = { latitude = 24.474796, longitude = 54.370576, display_name = "Abu Dhabi"}, ["Sihlat"] = { latitude = 24.336032, longitude = 56.498075, display_name = "Sihlat"}, ["Shufra"] = { latitude = 26.180000, longitude = 56.260000, display_name = "Shufra"}, ["Al Uwaynat"] = { latitude = 24.270000, longitude = 56.810000, display_name = "Al Uwaynat"}, ["Ras al Salam"] = { latitude = 26.030000, longitude = 56.350000, display_name = "Ras al Salam"}, ["New Madha"] = { latitude = 25.284452, longitude = 56.332977, display_name = "New Madha"}, ["Suhaylah"] = { latitude = 24.270000, longitude = 56.370000, display_name = "Suhaylah"}, ["Tabl"] = { latitude = 26.755208, longitude = 55.726098, display_name = "Tabl"}, ["Haqil"] = { latitude = 25.445791, longitude = 56.358072, display_name = "Haqil"}, ["`Aqqah"] = { latitude = 25.499746, longitude = 56.357323, display_name = "`Aqqah"}, ["Rul Dadna"] = { latitude = 25.554184, longitude = 56.345325, display_name = "Rul Dadna"}, ["Wab al Sebil"] = { latitude = 26.050000, longitude = 56.330000, display_name = "Wab al Sebil"}, ["Latifi"] = { latitude = 27.690078, longitude = 54.387140, display_name = "Latifi"}, ["Khur"] = { latitude = 27.645907, longitude = 54.345362, display_name = "Khur"}, ["Fudgha"] = { latitude = 26.120061, longitude = 56.129647, display_name = "Fudgha"}, ["Zaymi"] = { latitude = 24.450000, longitude = 56.280000, display_name = "Zaymi"}, ["Tazeyan-e Zir"] = { latitude = 27.291694, longitude = 56.153102, display_name = "Tazeyan-e Zir"}, ["Zabyat"] = { latitude = 24.350000, longitude = 56.370000, display_name = "Zabyat"}, ["Hadhf"] = { latitude = 24.789995, longitude = 56.010010, display_name = "Hadhf"}, ["Ar Rajmi"] = { latitude = 24.640000, longitude = 56.290000, display_name = "Ar Rajmi"}, ["Hansi"] = { latitude = 24.221653, longitude = 56.349073, display_name = "Hansi"}, ["Ghuzayyil"] = { latitude = 24.480000, longitude = 56.600000, display_name = "Ghuzayyil"}, ["Al Khywayriyyah"] = { latitude = 24.450000, longitude = 56.630000, display_name = "Al Khywayriyyah"}, ["Dubai"] = { latitude = 25.268352, longitude = 55.296196, display_name = "Dubai"}, ["Bayda"] = { latitude = 24.364497, longitude = 56.408018, display_name = "Bayda"}, ["Hadirah"] = { latitude = 24.380000, longitude = 56.740000, display_name = "Hadirah"}, ["Bu Baqarah"] = { latitude = 24.879549, longitude = 56.408535, display_name = "Bu Baqarah"}, ["As Sifah"] = { latitude = 24.830000, longitude = 56.430000, display_name = "As Sifah"}, ["Aswad"] = { latitude = 24.870000, longitude = 56.330000, display_name = "Aswad"}, ["Ash Sharjah"] = { latitude = 24.530000, longitude = 56.280000, display_name = "Ash Sharjah"}, ["Ghassah"] = { latitude = 26.240422, longitude = 56.318323, display_name = "Ghassah"}, ["Bat"] = { latitude = 24.520000, longitude = 56.280000, display_name = "Bat"}, ["Sal al Ala"] = { latitude = 26.050206, longitude = 56.374287, display_name = "Sal al Ala"}, ["Towla"] = { latitude = 26.972305, longitude = 56.223269, display_name = "Towla"}, ["Kaboli"] = { latitude = 26.951816, longitude = 56.209809, display_name = "Kaboli"}, ["Hamiri"] = { latitude = 26.944973, longitude = 56.202666, display_name = "Hamiri"}, ["Guran"] = { latitude = 26.725618, longitude = 55.617993, display_name = "Guran"}, ["Chahu Sharghi"] = { latitude = 26.691591, longitude = 55.508721, display_name = "Chahu Sharghi"}, ["Chahu Gharbi"] = { latitude = 26.683830, longitude = 55.482802, display_name = "Chahu Gharbi"}, ["Bandar-e-Doulab"] = { latitude = 26.676472, longitude = 55.460577, display_name = "Bandar-e-Doulab"}, ["Konar Siah"] = { latitude = 26.661945, longitude = 55.433527, display_name = "Konar Siah"}, ["Gori"] = { latitude = 26.632805, longitude = 55.365481, display_name = "Gori"}, ["Moradi"] = { latitude = 26.636508, longitude = 55.345794, display_name = "Moradi"}, ["Basa'idu"] = { latitude = 26.640952, longitude = 55.283018, display_name = "Basa'idu"}, ["Ash Shishah"] = { latitude = 26.260724, longitude = 56.393230, display_name = "Ash Shishah"}, ["Ras Salti Ali"] = { latitude = 26.213012, longitude = 56.233535, display_name = "Ras Salti Ali"}, ["Al Haqt"] = { latitude = 26.123078, longitude = 56.250251, display_name = "Al Haqt"}, ["Luwayb"] = { latitude = 25.770222, longitude = 56.226061, display_name = "Luwayb"}, ["Ras al Khaimah"] = { latitude = 25.761937, longitude = 55.935012, display_name = "Ras al Khaimah"}, ["Dibba al Fujairah"] = { latitude = 25.588498, longitude = 56.264200, display_name = "Dibba al Fujairah"}, ["Sharm"] = { latitude = 25.469538, longitude = 56.353863, display_name = "Sharm"}, ["Al Hawayah"] = { latitude = 25.372171, longitude = 56.345103, display_name = "Al Hawayah"}, ["Al Hutain"] = { latitude = 25.361035, longitude = 56.344583, display_name = "Al Hutain"}, ["Al Qadisa"] = { latitude = 25.337383, longitude = 56.346792, display_name = "Al Qadisa"}, ["Al Hayl"] = { latitude = 25.091823, longitude = 56.246812, display_name = "Al Hayl"}, ["Khalba"] = { latitude = 25.051499, longitude = 56.352167, display_name = "Khalba"}, ["Bani Jabir"] = { latitude = 24.776926, longitude = 56.449487, display_name = "Bani Jabir"}, ["Liwa"] = { latitude = 24.518034, longitude = 56.561109, display_name = "Liwa"}, ["At Tarayf"] = { latitude = 24.388600, longitude = 56.708776, display_name = "At Tarayf"}, ["Bahjat al Anzar"] = { latitude = 24.320845, longitude = 56.760297, display_name = "Bahjat al Anzar"}, ["Yal Burayk"] = { latitude = 24.012293, longitude = 57.035907, display_name = "Yal Burayk"}, ["Muzeira"] = { latitude = 24.839201, longitude = 56.035361, display_name = "Muzeira"}, ["Hatta"] = { latitude = 24.809872, longitude = 56.114054, display_name = "Hatta"}, ["Al Hayer"] = { latitude = 24.584657, longitude = 55.762584, display_name = "Al Hayer"}, ["Nahel"] = { latitude = 24.533336, longitude = 55.559137, display_name = "Nahel"}, ["Sweihan"] = { latitude = 24.488015, longitude = 55.343260, display_name = "Sweihan"}, ["Al Uthrat"] = { latitude = 24.353779, longitude = 56.050471, display_name = "Al Uthrat"}, ["Al Khubayn"] = { latitude = 24.323580, longitude = 56.100938, display_name = "Al Khubayn"}, ["Al Ashqar"] = { latitude = 24.309377, longitude = 56.132881, display_name = "Al Ashqar"}, ["Al Khrair"] = { latitude = 24.150677, longitude = 55.825251, display_name = "Al Khrair"}, ["Al Salamat"] = { latitude = 24.214094, longitude = 55.591883, display_name = "Al Salamat"}, ["Al Yahar North"] = { latitude = 24.241616, longitude = 55.557785, display_name = "Al Yahar North"}, ["Al Yahar South"] = { latitude = 24.202040, longitude = 55.534305, display_name = "Al Yahar South"}, ["Al Khazna"] = { latitude = 24.172745, longitude = 55.118734, display_name = "Al Khazna"}, ["Al Khatim"] = { latitude = 24.212594, longitude = 55.004704, display_name = "Al Khatim"}, ["Al Mafreq Industrial Area"] = { latitude = 24.272547, longitude = 54.591075, display_name = "Al Mafreq Industrial Area"}, ["Kang"] = { latitude = 26.596327, longitude = 54.936964, display_name = "Kang"}, ["Al Ain"] = { latitude = 24.222700, longitude = 55.692779, display_name = "Al Ain"}, ["Al Buraimi"] = { latitude = 24.261588, longitude = 55.792935, display_name = "Al Buraimi"}, ["Bandar Shenas"] = { latitude = 26.517778, longitude = 54.784529, display_name = "Bandar Shenas"}, ["Ghazi Castle"] = { latitude = 27.455000, longitude = 56.564467, display_name = "Ghazi Castle"}, ["Berentin"] = { latitude = 27.293682, longitude = 57.252660, display_name = "Berentin"}, ["Minab"] = { latitude = 27.138592, longitude = 57.072786, display_name = "Minab"}, ["The World Islands"] = { latitude = 25.224572, longitude = 55.164474, display_name = "The World Islands"}, ["Palm Jumeirah"] = { latitude = 25.120103, longitude = 55.129655, display_name = "Palm Jumeirah"}, [" Palm Jebel Ali"] = { latitude = 25.009022, longitude = 54.987897, display_name = " Palm Jebel Ali"}, ["Jebel Ali Port"] = { latitude = 24.974838, longitude = 55.070788, display_name = "Jebel Ali Port"}, ["Dubai Investments Park"] = { latitude = 24.981667, longitude = 55.179209, display_name = "Dubai Investments Park"}, ["Sir Abu Ny'air"] = { latitude = 25.226516, longitude = 54.217534, display_name = "Sir Abu Ny'air"}, ["Ghantoot"] = { latitude = 24.848710, longitude = 54.831340, display_name = "Ghantoot"}, ["EMAL Industrial City"] = { latitude = 24.791622, longitude = 54.721420, display_name = "EMAL Industrial City"}, ["Kizad"] = { latitude = 24.707434, longitude = 54.811143, display_name = "Kizad"}, ["Al Samkha"] = { latitude = 24.690096, longitude = 54.758042, display_name = "Al Samkha"}, ["Al Sharia"] = { latitude = 24.628021, longitude = 54.781774, display_name = "Al Sharia"}, ["Al Rahba"] = { latitude = 24.622200, longitude = 54.705903, display_name = "Al Rahba"}, ["Al Bahia"] = { latitude = 24.572522, longitude = 54.650999, display_name = "Al Bahia"}, ["Rawdat al Reef"] = { latitude = 24.508450, longitude = 54.718527, display_name = "Rawdat al Reef"}, ["Yas Island"] = { latitude = 24.490761, longitude = 54.612231, display_name = "Yas Island"}, ["Al Reef"] = { latitude = 24.454224, longitude = 54.670993, display_name = "Al Reef"}, ["Al Falah New Community"] = { latitude = 24.442883, longitude = 54.726928, display_name = "Al Falah New Community"}, ["Al Shamkha"] = { latitude = 24.381746, longitude = 54.709066, display_name = "Al Shamkha"}, ["Masdar City"] = { latitude = 24.427375, longitude = 54.625464, display_name = "Masdar City"}, ["Khalifa City"] = { latitude = 24.419081, longitude = 54.576085, display_name = "Khalifa City"}, ["Shakhbout City"] = { latitude = 24.364088, longitude = 54.632717, display_name = "Shakhbout City"}, ["Al Shawamekh"] = { latitude = 24.346064, longitude = 54.665524, display_name = "Al Shawamekh"}, ["Baniyas City"] = { latitude = 24.302115, longitude = 54.636742, display_name = "Baniyas City"}, ["Mohammed bin Zayed City"] = { latitude = 24.334233, longitude = 54.552368, display_name = "Mohammed bin Zayed City"}, ["Musaffah Industrial Area"] = { latitude = 24.348649, longitude = 54.491514, display_name = "Musaffah Industrial Area"}, ["Abu Dhabi Industrial Area"] = { latitude = 24.286298, longitude = 54.466256, display_name = "Abu Dhabi Industrial Area"}, ["Al Muqatra"] = { latitude = 24.215471, longitude = 54.462245, display_name = "Al Muqatra"}, ["Al Wathba"] = { latitude = 24.201053, longitude = 54.722964, display_name = "Al Wathba"}, ["Zayed Military City"] = { latitude = 24.503534, longitude = 54.872741, display_name = "Zayed Military City"}, ["Al Maqta"] = { latitude = 24.406182, longitude = 54.502626, display_name = "Al Maqta"}, ["Umm al Nar"] = { latitude = 24.434628, longitude = 54.500575, display_name = "Umm al Nar"}, ["Al Sa'adah"] = { latitude = 24.437465, longitude = 54.425519, display_name = "Al Sa'adah"}, ["Al Rawdah"] = { latitude = 24.421667, longitude = 54.437131, display_name = "Al Rawdah"}, ["Al Muzoun"] = { latitude = 24.411611, longitude = 54.430731, display_name = "Al Muzoun"}, ["Al Mushrif"] = { latitude = 24.432836, longitude = 54.394401, display_name = "Al Mushrif"}, ["Al Qurm"] = { latitude = 24.422772, longitude = 54.393330, display_name = "Al Qurm"}, ["Al Bateen"] = { latitude = 24.446453, longitude = 54.352998, display_name = "Al Bateen"}, ["Al Kasir"] = { latitude = 24.474630, longitude = 54.320160, display_name = "Al Kasir"}, ["Al Mina"] = { latitude = 24.519893, longitude = 54.370686, display_name = "Al Mina"}, ["Saadiyat Beach"] = { latitude = 24.543537, longitude = 54.435444, display_name = "Saadiyat Beach"}, ["Oman"] = { latitude = 26.128194, longitude = 56.245636, display_name = "Oman"}, ["Iran"] = { latitude = 27.697861, longitude = 55.847496, display_name = "Iran"}, ["Sir Abu Ny'air Island"] = { latitude = 25.234987, longitude = 54.215139, display_name = "Sir Abu Ny'air Island"}, ["Qeshm Island"] = { latitude = 26.818904, longitude = 55.901246, display_name = "Qeshm Island"}, ["Naif"] = { latitude = 25.272292, longitude = 55.311253, display_name = "Naif"}, ["Oud Metha"] = { latitude = 25.240796, longitude = 55.309122, display_name = "Oud Metha"}, ["Al Hudaiba"] = { latitude = 25.239402, longitude = 55.274196, display_name = "Al Hudaiba"}, ["Al Rigga"] = { latitude = 25.259060, longitude = 55.320567, display_name = "Al Rigga"}, ["Downtown Burj Khalifa"] = { latitude = 25.193372, longitude = 55.276023, display_name = "Downtown Burj Khalifa"}, ["Al Majaz"] = { latitude = 25.330634, longitude = 55.384092, display_name = "Al Majaz"}, ["Abu Hail"] = { latitude = 25.285615, longitude = 55.329179, display_name = "Abu Hail"}, ["Jumeirah"] = { latitude = 25.218588, longitude = 55.254966, display_name = "Jumeirah"}, ["Al Barsha"] = { latitude = 25.095399, longitude = 55.206466, display_name = "Al Barsha"}, ["Emirates Hills"] = { latitude = 25.068473, longitude = 55.171652, display_name = "Emirates Hills"}, ["Merdonu"] = { latitude = 27.322500, longitude = 54.474428, display_name = "Merdonu"}, ["Gowdegaz"] = { latitude = 27.300118, longitude = 54.496949, display_name = "Gowdegaz"}, ["Todruyeh"] = { latitude = 27.304580, longitude = 54.708803, display_name = "Todruyeh"}, ["Dehong"] = { latitude = 27.310218, longitude = 54.655915, display_name = "Dehong"}, ["Chah Benard"] = { latitude = 27.247221, longitude = 54.624649, display_name = "Chah Benard"}, ["Kookherd"] = { latitude = 27.088970, longitude = 54.493979, display_name = "Kookherd"}, ["Eelood"] = { latitude = 27.217003, longitude = 54.673731, display_name = "Eelood"}, ["Berkeh Lary"] = { latitude = 27.211954, longitude = 54.716274, display_name = "Berkeh Lary"}, ["Chah Dezdan"] = { latitude = 27.209354, longitude = 54.745048, display_name = "Chah Dezdan"}, ["Dehtal"] = { latitude = 27.210763, longitude = 54.785315, display_name = "Dehtal"}, ["Dashte Jayhun"] = { latitude = 27.276316, longitude = 55.079159, display_name = "Dashte Jayhun"}, ["Tang-e Dalan"] = { latitude = 27.333724, longitude = 55.093729, display_name = "Tang-e Dalan"}, ["Khoerde"] = { latitude = 27.781969, longitude = 54.420895, display_name = "Khoerde"}, ["Kanakh"] = { latitude = 26.899358, longitude = 55.389228, display_name = "Kanakh"}, ["Gavmiri"] = { latitude = 26.890173, longitude = 55.316569, display_name = "Gavmiri"}, ["Sayeh Khvosh"] = { latitude = 26.830745, longitude = 55.373321, display_name = "Sayeh Khvosh"}, ["Bandar e Pol"] = { latitude = 27.008738, longitude = 55.739849, display_name = "Bandar e Pol"}, ["Demilu"] = { latitude = 27.182336, longitude = 55.832653, display_name = "Demilu"}, ["Keshar e Bala"] = { latitude = 27.267073, longitude = 55.955476, display_name = "Keshar e Bala"}, ["Sargap"] = { latitude = 27.247097, longitude = 55.931328, display_name = "Sargap"}, ["Chamerdan"] = { latitude = 27.247713, longitude = 55.977965, display_name = "Chamerdan"}, ["Ghalat e Bala"] = { latitude = 27.319302, longitude = 56.099257, display_name = "Ghalat e Bala"}, ["Konaru"] = { latitude = 27.310582, longitude = 56.135679, display_name = "Konaru"}, ["Keshar e Paeen"] = { latitude = 27.230860, longitude = 55.906989, display_name = "Keshar e Paeen"}, ["Mogh Ahmad Paeen"] = { latitude = 27.155902, longitude = 55.882218, display_name = "Mogh Ahmad Paeen"}, ["Gachin Bala"] = { latitude = 27.125532, longitude = 55.872022, display_name = "Gachin Bala"}, ["Gachin Paeen"] = { latitude = 27.090410, longitude = 55.891007, display_name = "Gachin Paeen"}, ["Chahu"] = { latitude = 27.242365, longitude = 56.066446, display_name = "Chahu"}, ["Bostanu"] = { latitude = 27.080847, longitude = 55.996042, display_name = "Bostanu"}, ["Rajaei Port"] = { latitude = 27.102766, longitude = 56.060752, display_name = "Rajaei Port"}, ["Paiposht"] = { latitude = 26.879825, longitude = 55.928558, display_name = "Paiposht"}, ["Kuvei"] = { latitude = 26.946337, longitude = 56.005579, display_name = "Kuvei"}, ["Giahdan"] = { latitude = 26.923002, longitude = 56.069731, display_name = "Giahdan"}, ["Tourian"] = { latitude = 26.880562, longitude = 56.030528, display_name = "Tourian"}, ["Kousha"] = { latitude = 26.857848, longitude = 56.017255, display_name = "Kousha"}, ["Sarkhun"] = { latitude = 27.402982, longitude = 56.412849, display_name = "Sarkhun"}, ["Sarhez"] = { latitude = 27.580306, longitude = 56.098990, display_name = "Sarhez"}, ["Tola Industrial City"] = { latitude = 26.968887, longitude = 56.179724, display_name = "Tola Industrial City"}, ["Hajji Khademi"] = { latitude = 27.230324, longitude = 57.043880, display_name = "Hajji Khademi"}, ["Tirour"] = { latitude = 27.324834, longitude = 56.965962, display_name = "Tirour"}, ["Kormon"] = { latitude = 27.432573, longitude = 56.874832, display_name = "Kormon"}, ["Takht"] = { latitude = 27.493904, longitude = 56.656532, display_name = "Takht"}, ["Chahestan"] = { latitude = 27.519748, longitude = 56.752911, display_name = "Chahestan"}, ["Ramkan"] = { latitude = 26.867639, longitude = 56.043939, display_name = "Ramkan"}, ["Zirang"] = { latitude = 26.854199, longitude = 56.061282, display_name = "Zirang"}, ["Kerman"] = { latitude = 30.323533, longitude = 57.023611, display_name = "Kerman"}, ["Shiraz"] = { latitude = 29.559969, longitude = 52.618058, display_name = "Shiraz"}, ["Fasa"] = { latitude = 28.936639, longitude = 53.651068, display_name = "Fasa"}, ["Darab"] = { latitude = 28.753194, longitude = 54.548149, display_name = "Darab"}, ["Choghadak"] = { latitude = 28.985545, longitude = 51.032761, display_name = "Choghadak"}, ["Borazjan"] = { latitude = 29.266143, longitude = 51.212060, display_name = "Borazjan"}, ["Dalaki"] = { latitude = 29.430861, longitude = 51.295021, display_name = "Dalaki"}, ["Firuzabad"] = { latitude = 28.845604, longitude = 52.572724, display_name = "Firuzabad"}, ["Masiri"] = { latitude = 30.244395, longitude = 51.524174, display_name = "Masiri"}, ["Nurabad Mamasani"] = { latitude = 30.115407, longitude = 51.522963, display_name = "Nurabad Mamasani"}, ["Qaemiyeh"] = { latitude = 29.851002, longitude = 51.581912, display_name = "Qaemiyeh"}, ["Konartakhteh"] = { latitude = 29.532781, longitude = 51.396080, display_name = "Konartakhteh"}, ["Saadat Shahr"] = { latitude = 30.079995, longitude = 53.136456, display_name = "Saadat Shahr"}, ["Arsanjan"] = { latitude = 29.914968, longitude = 53.308976, display_name = "Arsanjan"}, ["Farashband"] = { latitude = 28.855378, longitude = 52.094644, display_name = "Farashband"}, ["Najafshahr"] = { latitude = 29.389703, longitude = 55.720301, display_name = "Najafshahr"}, ["Ab Pakhsh"] = { latitude = 29.360784, longitude = 51.071375, display_name = "Ab Pakhsh"}, ["Shabankareh"] = { latitude = 29.469683, longitude = 50.990973, display_name = "Shabankareh"}, ["Saadabad"] = { latitude = 29.384835, longitude = 51.116629, display_name = "Saadabad"}, ["Delvar"] = { latitude = 28.762873, longitude = 51.071923, display_name = "Delvar"}, ["Vahdatiyeh"] = { latitude = 29.482557, longitude = 51.243308, display_name = "Vahdatiyeh"}, ["Ahram"] = { latitude = 28.883028, longitude = 51.274202, display_name = "Ahram"}, ["Qir"] = { latitude = 28.482802, longitude = 53.035756, display_name = "Qir"}, ["Qotbabad"] = { latitude = 28.639302, longitude = 53.637805, display_name = "Qotbabad"}, ["Zahedshahr"] = { latitude = 28.748435, longitude = 53.804106, display_name = "Zahedshahr"}, ["Miandeh"] = { latitude = 28.716296, longitude = 53.855430, display_name = "Miandeh"}, ["Now Bandegan"] = { latitude = 28.854560, longitude = 53.825902, display_name = "Now Bandegan"}, ["Sheshdeh"] = { latitude = 28.948635, longitude = 53.995505, display_name = "Sheshdeh"}, ["Emamshahr"] = { latitude = 28.446670, longitude = 53.152544, display_name = "Emamshahr"}, ["Aliabad"] = { latitude = 28.789843, longitude = 51.055410, display_name = "Aliabad"}, ["Jaeinak"] = { latitude = 28.786242, longitude = 51.069132, display_name = "Jaeinak"}, ["Marvdasht"] = { latitude = 29.876291, longitude = 52.806290, display_name = "Marvdasht"}, ["Jahrom"] = { latitude = 28.496176, longitude = 53.559337, display_name = "Jahrom"}, ["Ij"] = { latitude = 29.020374, longitude = 54.247745, display_name = "Ij"}, ["Aviz"] = { latitude = 28.916223, longitude = 52.057212, display_name = "Aviz"}, ["Nudan"] = { latitude = 29.801949, longitude = 51.693599, display_name = "Nudan"}, ["Khesht"] = { latitude = 29.567944, longitude = 51.339531, display_name = "Khesht"}, } veafNamedPoints._citiesTheChannel = { ["Canterbury"] = { latitude = 51.280028, longitude = 1.080253, display_name = "Canterbury"}, ["Rochester"] = { latitude = 51.389062, longitude = 0.504935, display_name = "Rochester"}, ["Battle"] = { latitude = 50.917771, longitude = 0.483654, display_name = "Battle"}, ["Rye"] = { latitude = 50.951187, longitude = 0.732767, display_name = "Rye"}, ["Minnis Bay"] = { latitude = 51.380177, longitude = 1.285780, display_name = "Minnis Bay"}, ["Margate"] = { latitude = 51.389433, longitude = 1.382151, display_name = "Margate"}, ["Deal"] = { latitude = 51.223924, longitude = 1.402865, display_name = "Deal"}, ["Ramsgate"] = { latitude = 51.333473, longitude = 1.419648, display_name = "Ramsgate"}, ["Chatham"] = { latitude = 51.380484, longitude = 0.529276, display_name = "Chatham"}, ["Gillingham"] = { latitude = 51.387656, longitude = 0.545771, display_name = "Gillingham"}, ["Winchelsea"] = { latitude = 50.924390, longitude = 0.708636, display_name = "Winchelsea"}, ["Winchelsea Beach"] = { latitude = 50.915916, longitude = 0.723579, display_name = "Winchelsea Beach"}, ["Cliff End"] = { latitude = 50.888757, longitude = 0.683912, display_name = "Cliff End"}, ["Pett Level"] = { latitude = 50.889759, longitude = 0.687214, display_name = "Pett Level"}, ["Sheerness"] = { latitude = 51.439170, longitude = 0.758572, display_name = "Sheerness"}, ["Whitstable"] = { latitude = 51.360629, longitude = 1.024063, display_name = "Whitstable"}, ["Dover"] = { latitude = 51.125128, longitude = 1.313423, display_name = "Dover"}, ["Elham"] = { latitude = 51.153771, longitude = 1.110679, display_name = "Elham"}, ["Wingham"] = { latitude = 51.272594, longitude = 1.215429, display_name = "Wingham"}, ["Eastry"] = { latitude = 51.247183, longitude = 1.307598, display_name = "Eastry"}, ["Maidstone"] = { latitude = 51.274826, longitude = 0.523165, display_name = "Maidstone"}, ["Loose"] = { latitude = 51.241150, longitude = 0.516822, display_name = "Loose"}, ["Aylesford"] = { latitude = 51.303717, longitude = 0.482638, display_name = "Aylesford"}, ["Headcorn"] = { latitude = 51.168427, longitude = 0.627622, display_name = "Headcorn"}, ["Ashford"] = { latitude = 51.148555, longitude = 0.872257, display_name = "Ashford"}, ["Wye"] = { latitude = 51.183248, longitude = 0.936922, display_name = "Wye"}, ["Folkestone"] = { latitude = 51.079134, longitude = 1.179407, display_name = "Folkestone"}, ["Burham"] = { latitude = 51.328820, longitude = 0.482294, display_name = "Burham"}, ["Herne Bay"] = { latitude = 51.371951, longitude = 1.130695, display_name = "Herne Bay"}, ["Hawkinge"] = { latitude = 51.118042, longitude = 1.166441, display_name = "Hawkinge"}, ["Boughton Lees"] = { latitude = 51.188913, longitude = 0.892375, display_name = "Boughton Lees"}, ["Challock"] = { latitude = 51.219886, longitude = 0.876510, display_name = "Challock"}, ["Sevington"] = { latitude = 51.133271, longitude = 0.904052, display_name = "Sevington"}, ["Borstal"] = { latitude = 51.374409, longitude = 0.485466, display_name = "Borstal"}, ["Brede"] = { latitude = 50.935783, longitude = 0.596550, display_name = "Brede"}, ["Chartham"] = { latitude = 51.254268, longitude = 1.020390, display_name = "Chartham"}, ["Charing"] = { latitude = 51.211424, longitude = 0.794972, display_name = "Charing"}, ["Harrietsham"] = { latitude = 51.243350, longitude = 0.671526, display_name = "Harrietsham"}, ["Hinxhill"] = { latitude = 51.145818, longitude = 0.928268, display_name = "Hinxhill"}, ["Smeeth"] = { latitude = 51.118147, longitude = 0.959869, display_name = "Smeeth"}, ["Mersham"] = { latitude = 51.119423, longitude = 0.932362, display_name = "Mersham"}, ["Hythe"] = { latitude = 51.069142, longitude = 1.084163, display_name = "Hythe"}, ["Stowting"] = { latitude = 51.136687, longitude = 1.035279, display_name = "Stowting"}, ["Lyminge"] = { latitude = 51.128744, longitude = 1.087791, display_name = "Lyminge"}, ["Molash"] = { latitude = 51.230119, longitude = 0.898823, display_name = "Molash"}, ["Godmersham"] = { latitude = 51.215166, longitude = 0.950298, display_name = "Godmersham"}, ["Lydd"] = { latitude = 50.951567, longitude = 0.907511, display_name = "Lydd"}, ["Pluckley"] = { latitude = 51.175264, longitude = 0.754645, display_name = "Pluckley"}, ["Hamstreet"] = { latitude = 51.064901, longitude = 0.855344, display_name = "Hamstreet"}, ["Woodchurch"] = { latitude = 51.076741, longitude = 0.775834, display_name = "Woodchurch"}, ["Tenterden"] = { latitude = 51.070096, longitude = 0.688827, display_name = "Tenterden"}, ["High Halden"] = { latitude = 51.103550, longitude = 0.711234, display_name = "High Halden"}, ["Faversham"] = { latitude = 51.314409, longitude = 0.891189, display_name = "Faversham"}, ["Shepherdswell"] = { latitude = 51.185588, longitude = 1.231137, display_name = "Shepherdswell"}, ["West Malling"] = { latitude = 51.295403, longitude = 0.409461, display_name = "West Malling"}, ["Little Chart"] = { latitude = 51.179579, longitude = 0.780473, display_name = "Little Chart"}, ["Minster"] = { latitude = 51.333898, longitude = 1.316084, display_name = "Minster"}, ["Acol"] = { latitude = 51.357756, longitude = 1.311721, display_name = "Acol"}, ["Alkham"] = { latitude = 51.135740, longitude = 1.223693, display_name = "Alkham"}, ["Adisham"] = { latitude = 51.239153, longitude = 1.188543, display_name = "Adisham"}, ["Ditton"] = { latitude = 51.296383, longitude = 0.453567, display_name = "Ditton"}, ["Biddenden"] = { latitude = 51.115357, longitude = 0.642861, display_name = "Biddenden"}, ["Smarden"] = { latitude = 51.148932, longitude = 0.686724, display_name = "Smarden"}, ["Preston"] = { latitude = 51.303539, longitude = 1.227388, display_name = "Preston"}, ["Sandwich"] = { latitude = 51.275253, longitude = 1.340831, display_name = "Sandwich"}, ["St. Margaret's at Cliffe"] = { latitude = 51.154196, longitude = 1.372227, display_name = "St. Margaret's at Cliffe"}, ["Whitfield"] = { latitude = 51.159496, longitude = 1.288821, display_name = "Whitfield"}, ["Detling"] = { latitude = 51.294758, longitude = 0.569084, display_name = "Detling"}, ["Ash"] = { latitude = 51.280986, longitude = 1.280306, display_name = "Ash"}, ["New Romney"] = { latitude = 50.985120, longitude = 0.942657, display_name = "New Romney"}, ["Dymchurch"] = { latitude = 51.026329, longitude = 0.993650, display_name = "Dymchurch"}, ["Dungeness"] = { latitude = 50.914178, longitude = 0.972894, display_name = "Dungeness"}, ["Blean"] = { latitude = 51.309226, longitude = 1.041400, display_name = "Blean"}, ["Bramling"] = { latitude = 51.266099, longitude = 1.191608, display_name = "Bramling"}, ["Upstreet"] = { latitude = 51.323878, longitude = 1.195885, display_name = "Upstreet"}, ["Cliffsend"] = { latitude = 51.328048, longitude = 1.365472, display_name = "Cliffsend"}, ["Broadstairs"] = { latitude = 51.358676, longitude = 1.440785, display_name = "Broadstairs"}, ["Wickhambreaux"] = { latitude = 51.285060, longitude = 1.183971, display_name = "Wickhambreaux"}, ["Pegwell"] = { latitude = 51.327407, longitude = 1.392171, display_name = "Pegwell"}, ["Chilton"] = { latitude = 51.330792, longitude = 1.391433, display_name = "Chilton"}, ["Manston"] = { latitude = 51.345980, longitude = 1.369626, display_name = "Manston"}, ["Westwood"] = { latitude = 51.361166, longitude = 1.394531, display_name = "Westwood"}, ["Northiam"] = { latitude = 50.990862, longitude = 0.606276, display_name = "Northiam"}, ["St Marys Bay"] = { latitude = 51.011714, longitude = 0.978640, display_name = "St Marys Bay"}, ["Lympne"] = { latitude = 51.075782, longitude = 1.027252, display_name = "Lympne"}, ["Brabourne Lees"] = { latitude = 51.125756, longitude = 0.972561, display_name = "Brabourne Lees"}, ["Lymbridge Green"] = { latitude = 51.156119, longitude = 1.037883, display_name = "Lymbridge Green"}, ["Dunkirk"] = { latitude = 51.292033, longitude = 0.977125, display_name = "Dunkirk"}, ["Rough Common"] = { latitude = 51.291596, longitude = 1.047419, display_name = "Rough Common"}, ["Seasalter"] = { latitude = 51.348220, longitude = 1.005063, display_name = "Seasalter"}, ["Tankerton"] = { latitude = 51.364112, longitude = 1.043862, display_name = "Tankerton"}, ["Swalecliffe"] = { latitude = 51.366045, longitude = 1.068187, display_name = "Swalecliffe"}, ["Herne"] = { latitude = 51.350351, longitude = 1.133792, display_name = "Herne"}, ["Westbere"] = { latitude = 51.308149, longitude = 1.144101, display_name = "Westbere"}, ["Wouldham"] = { latitude = 51.351228, longitude = 0.460243, display_name = "Wouldham"}, ["Stelling Minnis"] = { latitude = 51.179301, longitude = 1.068386, display_name = "Stelling Minnis"}, ["Saltwood"] = { latitude = 51.080165, longitude = 1.081790, display_name = "Saltwood"}, ["Sandgate"] = { latitude = 51.074379, longitude = 1.148713, display_name = "Sandgate"}, ["Hersden"] = { latitude = 51.314705, longitude = 1.160170, display_name = "Hersden"}, ["Broomfield"] = { latitude = 51.357427, longitude = 1.154917, display_name = "Broomfield"}, ["St Nicholas-at-Wade"] = { latitude = 51.352050, longitude = 1.253523, display_name = "St Nicholas-at-Wade"}, ["Monkton"] = { latitude = 51.338792, longitude = 1.281998, display_name = "Monkton"}, ["Woodnesborough"] = { latitude = 51.265848, longitude = 1.305345, display_name = "Woodnesborough"}, ["Elvington"] = { latitude = 51.206252, longitude = 1.248205, display_name = "Elvington"}, ["Eythorne"] = { latitude = 51.198082, longitude = 1.266917, display_name = "Eythorne"}, ["Stockbury"] = { latitude = 51.327200, longitude = 0.641439, display_name = "Stockbury"}, ["Hartlip"] = { latitude = 51.348055, longitude = 0.639520, display_name = "Hartlip"}, ["Halling"] = { latitude = 51.354593, longitude = 0.444094, display_name = "Halling"}, ["Birling"] = { latitude = 51.319026, longitude = 0.410614, display_name = "Birling"}, ["Strood"] = { latitude = 51.395857, longitude = 0.495080, display_name = "Strood"}, ["Westgate-on-Sea"] = { latitude = 51.381577, longitude = 1.337211, display_name = "Westgate-on-Sea"}, ["Etchingham"] = { latitude = 51.008586, longitude = 0.438025, display_name = "Etchingham"}, ["Hurst Green"] = { latitude = 51.018451, longitude = 0.469524, display_name = "Hurst Green"}, ["Lydden"] = { latitude = 51.162264, longitude = 1.241301, display_name = "Lydden"}, ["Temple Ewell"] = { latitude = 51.152250, longitude = 1.269622, display_name = "Temple Ewell"}, ["Kearsney"] = { latitude = 51.148482, longitude = 1.266451, display_name = "Kearsney"}, ["Ringwould"] = { latitude = 51.183706, longitude = 1.376369, display_name = "Ringwould"}, ["Ripple"] = { latitude = 51.200491, longitude = 1.357389, display_name = "Ripple"}, ["Walmer"] = { latitude = 51.206888, longitude = 1.399915, display_name = "Walmer"}, ["Northbourne"] = { latitude = 51.220424, longitude = 1.337825, display_name = "Northbourne"}, ["Sholden"] = { latitude = 51.224973, longitude = 1.375277, display_name = "Sholden"}, ["Worth"] = { latitude = 51.256253, longitude = 1.348298, display_name = "Worth"}, ["Stone Cross"] = { latitude = 51.265876, longitude = 1.338865, display_name = "Stone Cross"}, ["Bossingham"] = { latitude = 51.200445, longitude = 1.076820, display_name = "Bossingham"}, ["Petham"] = { latitude = 51.222798, longitude = 1.045879, display_name = "Petham"}, ["Robertsbridge"] = { latitude = 50.985640, longitude = 0.474452, display_name = "Robertsbridge"}, ["Kemsley"] = { latitude = 51.363471, longitude = 0.739047, display_name = "Kemsley"}, ["Harbledown"] = { latitude = 51.281829, longitude = 1.056575, display_name = "Harbledown"}, ["Orlestone"] = { latitude = 51.076644, longitude = 0.853228, display_name = "Orlestone"}, ["Horsmonden"] = { latitude = 51.139145, longitude = 0.431837, display_name = "Horsmonden"}, ["Denton"] = { latitude = 51.181449, longitude = 1.169768, display_name = "Denton"}, ["Goodnestone"] = { latitude = 51.246747, longitude = 1.230529, display_name = "Goodnestone"}, ["Bexhill-on-Sea"] = { latitude = 50.842438, longitude = 0.467572, display_name = "Bexhill-on-Sea"}, ["Hastings"] = { latitude = 50.855389, longitude = 0.582470, display_name = "Hastings"}, ["Sutton Valence"] = { latitude = 51.212634, longitude = 0.592560, display_name = "Sutton Valence"}, ["Egerton"] = { latitude = 51.195112, longitude = 0.729017, display_name = "Egerton"}, ["Walderslade"] = { latitude = 51.342945, longitude = 0.526780, display_name = "Walderslade"}, ["Shorne"] = { latitude = 51.413153, longitude = 0.433041, display_name = "Shorne"}, ["Cuxton"] = { latitude = 51.375046, longitude = 0.455713, display_name = "Cuxton"}, ["Higham"] = { latitude = 51.415105, longitude = 0.459730, display_name = "Higham"}, ["Bluebell Hill"] = { latitude = 51.332465, longitude = 0.505855, display_name = "Bluebell Hill"}, ["Bredhurst"] = { latitude = 51.331700, longitude = 0.579602, display_name = "Bredhurst"}, ["Upper Upnor"] = { latitude = 51.405943, longitude = 0.525429, display_name = "Upper Upnor"}, ["Wateringbury"] = { latitude = 51.255749, longitude = 0.416423, display_name = "Wateringbury"}, ["East Malling"] = { latitude = 51.287380, longitude = 0.438542, display_name = "East Malling"}, ["Lower Upnor"] = { latitude = 51.412433, longitude = 0.530373, display_name = "Lower Upnor"}, ["Frindsbury"] = { latitude = 51.400196, longitude = 0.506504, display_name = "Frindsbury"}, ["Wainscott"] = { latitude = 51.411234, longitude = 0.510528, display_name = "Wainscott"}, ["Hale"] = { latitude = 51.363531, longitude = 0.555123, display_name = "Hale"}, ["Lower Rainham"] = { latitude = 51.379150, longitude = 0.605817, display_name = "Lower Rainham"}, ["Moor Street"] = { latitude = 51.358628, longitude = 0.628863, display_name = "Moor Street"}, ["Eccles"] = { latitude = 51.317774, longitude = 0.481066, display_name = "Eccles"}, ["Hempstead"] = { latitude = 51.351133, longitude = 0.569747, display_name = "Hempstead"}, ["Boxley"] = { latitude = 51.301603, longitude = 0.543120, display_name = "Boxley"}, ["Thurnham"] = { latitude = 51.291306, longitude = 0.591831, display_name = "Thurnham"}, ["Royal British Legion Village"] = { latitude = 51.293880, longitude = 0.475714, display_name = "Royal British Legion Village"}, ["Lower Halstow"] = { latitude = 51.372477, longitude = 0.669655, display_name = "Lower Halstow"}, ["Upchurch"] = { latitude = 51.376198, longitude = 0.649188, display_name = "Upchurch"}, ["Newington"] = { latitude = 51.351563, longitude = 0.665701, display_name = "Newington"}, ["Yalding"] = { latitude = 51.224363, longitude = 0.431451, display_name = "Yalding"}, ["Nettlestead"] = { latitude = 51.243649, longitude = 0.412021, display_name = "Nettlestead"}, ["Hunton"] = { latitude = 51.218873, longitude = 0.459449, display_name = "Hunton"}, ["West Farleigh"] = { latitude = 51.251037, longitude = 0.455152, display_name = "West Farleigh"}, ["East Farleigh"] = { latitude = 51.253071, longitude = 0.483908, display_name = "East Farleigh"}, ["St Michaels"] = { latitude = 51.082873, longitude = 0.692405, display_name = "St Michaels"}, ["Broad Oak"] = { latitude = 50.950030, longitude = 0.599972, display_name = "Broad Oak"}, ["Crowhurst"] = { latitude = 50.882407, longitude = 0.497905, display_name = "Crowhurst"}, ["Ninfield"] = { latitude = 50.888399, longitude = 0.418847, display_name = "Ninfield"}, ["Westfield"] = { latitude = 50.908967, longitude = 0.576927, display_name = "Westfield"}, ["Icklesham"] = { latitude = 50.916764, longitude = 0.666258, display_name = "Icklesham"}, ["Newingreen"] = { latitude = 51.084584, longitude = 1.034409, display_name = "Newingreen"}, ["Bethersden"] = { latitude = 51.127249, longitude = 0.752789, display_name = "Bethersden"}, ["Netherfield"] = { latitude = 50.943373, longitude = 0.435069, display_name = "Netherfield"}, ["Hollingbourne"] = { latitude = 51.265694, longitude = 0.641462, display_name = "Hollingbourne"}, ["Birchington"] = { latitude = 51.373802, longitude = 1.307125, display_name = "Birchington"}, ["Sedlescombe"] = { latitude = 50.940424, longitude = 0.529209, display_name = "Sedlescombe"}, ["Sedlescombe Street"] = { latitude = 50.932365, longitude = 0.533243, display_name = "Sedlescombe Street"}, ["Fairlight"] = { latitude = 50.875316, longitude = 0.662418, display_name = "Fairlight"}, ["St Leonards"] = { latitude = 50.855726, longitude = 0.548014, display_name = "St Leonards"}, ["Old Hawkinge"] = { latitude = 51.114871, longitude = 1.184821, display_name = "Old Hawkinge"}, ["Hawkhurst"] = { latitude = 51.046963, longitude = 0.508255, display_name = "Hawkhurst"}, ["Ticehurst"] = { latitude = 51.045772, longitude = 0.413924, display_name = "Ticehurst"}, ["Pett"] = { latitude = 50.895003, longitude = 0.669064, display_name = "Pett"}, ["Boughton Monchelsea"] = { latitude = 51.232170, longitude = 0.530222, display_name = "Boughton Monchelsea"}, ["Catsfield"] = { latitude = 50.897500, longitude = 0.450511, display_name = "Catsfield"}, ["East Guldeford"] = { latitude = 50.958813, longitude = 0.755803, display_name = "East Guldeford"}, ["Rye Harbour"] = { latitude = 50.938298, longitude = 0.759768, display_name = "Rye Harbour"}, ["Camber"] = { latitude = 50.935201, longitude = 0.795683, display_name = "Camber"}, ["Peasmarsh"] = { latitude = 50.972788, longitude = 0.690662, display_name = "Peasmarsh"}, ["Oversland"] = { latitude = 51.279307, longitude = 0.951784, display_name = "Oversland"}, ["Penenden Heath"] = { latitude = 51.288097, longitude = 0.538713, display_name = "Penenden Heath"}, ["Sittingbourne"] = { latitude = 51.339737, longitude = 0.734232, display_name = "Sittingbourne"}, ["Snodland"] = { latitude = 51.329623, longitude = 0.442632, display_name = "Snodland"}, ["River"] = { latitude = 51.143546, longitude = 1.274147, display_name = "River"}, ["Sole Street"] = { latitude = 51.204643, longitude = 0.999889, display_name = "Sole Street"}, ["Shorne Ridgeway"] = { latitude = 51.408383, longitude = 0.431770, display_name = "Shorne Ridgeway"}, ["Flimwell"] = { latitude = 51.054847, longitude = 0.446861, display_name = "Flimwell"}, ["Tyler Hill"] = { latitude = 51.307809, longitude = 1.069309, display_name = "Tyler Hill"}, ["Chestfield"] = { latitude = 51.353758, longitude = 1.067317, display_name = "Chestfield"}, ["Boughton Malherbe"] = { latitude = 51.215256, longitude = 0.694976, display_name = "Boughton Malherbe"}, ["Iwade"] = { latitude = 51.376599, longitude = 0.727962, display_name = "Iwade"}, ["Bobbing"] = { latitude = 51.353954, longitude = 0.709361, display_name = "Bobbing"}, ["Rodmersham Green"] = { latitude = 51.319209, longitude = 0.747733, display_name = "Rodmersham Green"}, ["Bredgar"] = { latitude = 51.313023, longitude = 0.696771, display_name = "Bredgar"}, ["Milstead"] = { latitude = 51.295834, longitude = 0.728404, display_name = "Milstead"}, ["Borden"] = { latitude = 51.333949, longitude = 0.702481, display_name = "Borden"}, ["Tunstall"] = { latitude = 51.323568, longitude = 0.717667, display_name = "Tunstall"}, ["Wormshill"] = { latitude = 51.283577, longitude = 0.694398, display_name = "Wormshill"}, ["Sarre"] = { latitude = 51.338879, longitude = 1.238516, display_name = "Sarre"}, ["Capel-le-Ferne"] = { latitude = 51.102164, longitude = 1.211693, display_name = "Capel-le-Ferne"}, ["Playden"] = { latitude = 50.962397, longitude = 0.732905, display_name = "Playden"}, ["Mountfield"] = { latitude = 50.956032, longitude = 0.479411, display_name = "Mountfield"}, ["Seabrook"] = { latitude = 51.072958, longitude = 1.118642, display_name = "Seabrook"}, ["Conyer"] = { latitude = 51.347093, longitude = 0.816969, display_name = "Conyer"}, ["Guestling"] = { latitude = 50.890229, longitude = 0.630193, display_name = "Guestling"}, ["Staplecross"] = { latitude = 50.973103, longitude = 0.539617, display_name = "Staplecross"}, ["Kingsdown"] = { latitude = 51.186841, longitude = 1.402737, display_name = "Kingsdown"}, ["Sandwich Bay"] = { latitude = 51.267788, longitude = 1.385374, display_name = "Sandwich Bay"}, ["Minster on Sea"] = { latitude = 51.420236, longitude = 0.803194, display_name = "Minster on Sea"}, ["Tonbridge"] = { latitude = 51.198712, longitude = 0.277477, display_name = "Tonbridge"}, ["Royal Tunbridge Wells"] = { latitude = 51.138667, longitude = 0.261915, display_name = "Royal Tunbridge Wells"}, ["Sevenoaks"] = { latitude = 51.275888, longitude = 0.182776, display_name = "Sevenoaks"}, ["Oxted"] = { latitude = 51.260298, longitude = 0.031627, display_name = "Oxted"}, ["East Grinstead"] = { latitude = 51.129870, longitude = -0.010519, display_name = "East Grinstead"}, ["Crowborough"] = { latitude = 51.057392, longitude = 0.159431, display_name = "Crowborough"}, ["Heathfield"] = { latitude = 50.965302, longitude = 0.250549, display_name = "Heathfield"}, ["Burwash Common"] = { latitude = 50.984654, longitude = 0.316699, display_name = "Burwash Common"}, ["Horam"] = { latitude = 50.933414, longitude = 0.246359, display_name = "Horam"}, ["Hailsham"] = { latitude = 50.866286, longitude = 0.258547, display_name = "Hailsham"}, ["Windmill Hill"] = { latitude = 50.883438, longitude = 0.348513, display_name = "Windmill Hill"}, ["Eastbourne"] = { latitude = 50.769830, longitude = 0.282343, display_name = "Eastbourne"}, ["Dunkirk"] = { latitude = 51.034771, longitude = 2.377253, display_name = "Dunkirk"}, ["Coudekerque-Branche"] = { latitude = 51.020878, longitude = 2.389432, display_name = "Coudekerque-Branche"}, ["Bailleul"] = { latitude = 50.739667, longitude = 2.734929, display_name = "Bailleul"}, ["Grande-Synthe"] = { latitude = 51.013481, longitude = 2.302997, display_name = "Grande-Synthe"}, ["Le Portel"] = { latitude = 50.707458, longitude = 1.573716, display_name = "Le Portel"}, ["Isbergues"] = { latitude = 50.621124, longitude = 2.457463, display_name = "Isbergues"}, ["Longuenesse"] = { latitude = 50.737100, longitude = 2.248330, display_name = "Longuenesse"}, ["Gravelines"] = { latitude = 50.987070, longitude = 2.127312, display_name = "Gravelines"}, ["Calais"] = { latitude = 50.948800, longitude = 1.874680, display_name = "Calais"}, ["Saint-Martin-Boulogne"] = { latitude = 50.722905, longitude = 1.646774, display_name = "Saint-Martin-Boulogne"}, ["Saint-Omer"] = { latitude = 50.752191, longitude = 2.254075, display_name = "Saint-Omer"}, ["Hazebrouck"] = { latitude = 50.722611, longitude = 2.536033, display_name = "Hazebrouck"}, ["Boulogne-sur-Mer"] = { latitude = 50.725998, longitude = 1.611877, display_name = "Boulogne-sur-Mer"}, ["Ghyvelde"] = { latitude = 51.051857, longitude = 2.526378, display_name = "Ghyvelde"}, ["Aire-sur-la-Lys"] = { latitude = 50.639594, longitude = 2.400060, display_name = "Aire-sur-la-Lys"}, ["Vieux-Berquin"] = { latitude = 50.694805, longitude = 2.643792, display_name = "Vieux-Berquin"}, ["Cappelle-la-Grande"] = { latitude = 50.997722, longitude = 2.367794, display_name = "Cappelle-la-Grande"}, ["Bergues"] = { latitude = 50.968389, longitude = 2.432525, display_name = "Bergues"}, ["Vendin-les-Bethune"] = { latitude = 50.546200, longitude = 2.604230, display_name = "Vendin-les-Bethune"}, ["Desvres"] = { latitude = 50.667874, longitude = 1.834827, display_name = "Desvres"}, ["Armbouts-Cappel"] = { latitude = 50.978044, longitude = 2.351581, display_name = "Armbouts-Cappel"}, ["Cassel"] = { latitude = 50.800001, longitude = 2.486831, display_name = "Cassel"}, ["Steenvoorde"] = { latitude = 50.810126, longitude = 2.581641, display_name = "Steenvoorde"}, ["Teteghem"] = { latitude = 51.016805, longitude = 2.441295, display_name = "Teteghem"}, ["Eperlecques"] = { latitude = 50.815400, longitude = 2.158610, display_name = "Eperlecques"}, ["Camiers"] = { latitude = 50.563800, longitude = 1.614410, display_name = "Camiers"}, ["Chocques"] = { latitude = 50.539186, longitude = 2.569606, display_name = "Chocques"}, ["Meteren"] = { latitude = 50.740908, longitude = 2.691174, display_name = "Meteren"}, ["Blendecques"] = { latitude = 50.717084, longitude = 2.281795, display_name = "Blendecques"}, ["Marquise"] = { latitude = 50.812582, longitude = 1.699554, display_name = "Marquise"}, ["Lestrem"] = { latitude = 50.621621, longitude = 2.685753, display_name = "Lestrem"}, ["Saint-Etienne-au-Mont"] = { latitude = 50.681686, longitude = 1.626043, display_name = "Saint-Etienne-au-Mont"}, ["Saint-Folquin"] = { latitude = 50.945910, longitude = 2.123789, display_name = "Saint-Folquin"}, ["Beuvry"] = { latitude = 50.533719, longitude = 2.686479, display_name = "Beuvry"}, ["Bourbourg"] = { latitude = 50.946662, longitude = 2.197370, display_name = "Bourbourg"}, ["Rinxent"] = { latitude = 50.806400, longitude = 1.739970, display_name = "Rinxent"}, ["Morbecque"] = { latitude = 50.691853, longitude = 2.515558, display_name = "Morbecque"}, ["Saint-Venant"] = { latitude = 50.623765, longitude = 2.549267, display_name = "Saint-Venant"}, ["Samer"] = { latitude = 50.639272, longitude = 1.746919, display_name = "Samer"}, ["Saint-Leonard"] = { latitude = 50.688814, longitude = 1.627086, display_name = "Saint-Leonard"}, ["Marck"] = { latitude = 50.949000, longitude = 1.951990, display_name = "Marck"}, ["Ambleteuse"] = { latitude = 50.811233, longitude = 1.606669, display_name = "Ambleteuse"}, ["Renescure"] = { latitude = 50.727611, longitude = 2.369521, display_name = "Renescure"}, ["Hondschoote"] = { latitude = 50.980856, longitude = 2.586264, display_name = "Hondschoote"}, ["Coulogne"] = { latitude = 50.925743, longitude = 1.884090, display_name = "Coulogne"}, ["Grand-Fort-Philippe"] = { latitude = 51.001446, longitude = 2.104597, display_name = "Grand-Fort-Philippe"}, ["Equihen-Plage"] = { latitude = 50.679393, longitude = 1.571665, display_name = "Equihen-Plage"}, ["Guines"] = { latitude = 50.869086, longitude = 1.870441, display_name = "Guines"}, ["Merville"] = { latitude = 50.643658, longitude = 2.638750, display_name = "Merville"}, ["Loon-Plage"] = { latitude = 50.994610, longitude = 2.219246, display_name = "Loon-Plage"}, ["Locon"] = { latitude = 50.570500, longitude = 2.666170, display_name = "Locon"}, ["Lillers"] = { latitude = 50.560600, longitude = 2.476040, display_name = "Lillers"}, ["Wimille"] = { latitude = 50.764109, longitude = 1.630328, display_name = "Wimille"}, ["Gonnehem"] = { latitude = 50.562287, longitude = 2.573843, display_name = "Gonnehem"}, ["Racquinghem"] = { latitude = 50.692974, longitude = 2.356774, display_name = "Racquinghem"}, ["Watten"] = { latitude = 50.832402, longitude = 2.212066, display_name = "Watten"}, ["La Gorgue"] = { latitude = 50.637260, longitude = 2.713401, display_name = "La Gorgue"}, ["Esquelbecq"] = { latitude = 50.886213, longitude = 2.430913, display_name = "Esquelbecq"}, ["Audruicq"] = { latitude = 50.878649, longitude = 2.075236, display_name = "Audruicq"}, ["Saint-Martin-au-Laert"] = { latitude = 50.754529, longitude = 2.234968, display_name = "Saint-Martin-au-Laert"}, ["Condette"] = { latitude = 50.649000, longitude = 1.642110, display_name = "Condette"}, ["Arques"] = { latitude = 50.739664, longitude = 2.306207, display_name = "Arques"}, ["Oye-Plage"] = { latitude = 50.980900, longitude = 2.041980, display_name = "Oye-Plage"}, ["Bray-Dunes"] = { latitude = 51.071002, longitude = 2.524513, display_name = "Bray-Dunes"}, ["Leffrinckoucke"] = { latitude = 51.050940, longitude = 2.438674, display_name = "Leffrinckoucke"}, ["Hoymille"] = { latitude = 50.973018, longitude = 2.448505, display_name = "Hoymille"}, ["Wizernes"] = { latitude = 50.711594, longitude = 2.230686, display_name = "Wizernes"}, ["Burbure"] = { latitude = 50.537016, longitude = 2.465564, display_name = "Burbure"}, ["Neufchatel-Hardelot"] = { latitude = 50.618849, longitude = 1.630616, display_name = "Neufchatel-Hardelot"}, ["La Couture"] = { latitude = 50.580187, longitude = 2.712909, display_name = "La Couture"}, ["Lumbres"] = { latitude = 50.705136, longitude = 2.121790, display_name = "Lumbres"}, ["Ardres"] = { latitude = 50.853632, longitude = 1.978607, display_name = "Ardres"}, ["Estaires"] = { latitude = 50.644026, longitude = 2.722651, display_name = "Estaires"}, ["Richebourg"] = { latitude = 50.579253, longitude = 2.739930, display_name = "Richebourg"}, ["Allouagne"] = { latitude = 50.532500, longitude = 2.507060, display_name = "Allouagne"}, ["Annezin"] = { latitude = 50.541500, longitude = 2.619300, display_name = "Annezin"}, ["Killem"] = { latitude = 50.957849, longitude = 2.562633, display_name = "Killem"}, ["Watou"] = { latitude = 50.858554, longitude = 2.620280, display_name = "Watou"}, ["Saint-Tricat"] = { latitude = 50.893532, longitude = 1.831895, display_name = "Saint-Tricat"}, ["Escalles"] = { latitude = 50.917985, longitude = 1.713605, display_name = "Escalles"}, ["Sint-Idesbald"] = { latitude = 51.109274, longitude = 2.612880, display_name = "Sint-Idesbald"}, ["De Panne"] = { latitude = 51.098831, longitude = 2.592409, display_name = "De Panne"}, ["Poperinge"] = { latitude = 50.855665, longitude = 2.726496, display_name = "Poperinge"}, ["Winnezeele"] = { latitude = 50.840061, longitude = 2.551238, display_name = "Winnezeele"}, ["Les Moeres"] = { latitude = 51.014607, longitude = 2.548824, display_name = "Les Moeres"}, ["Wylder"] = { latitude = 50.911643, longitude = 2.493981, display_name = "Wylder"}, ["Alveringem"] = { latitude = 51.011998, longitude = 2.710503, display_name = "Alveringem"}, ["La Capelle-les-Boulogne"] = { latitude = 50.730602, longitude = 1.701125, display_name = "La Capelle-les-Boulogne"}, ["Fletre"] = { latitude = 50.753348, longitude = 2.645652, display_name = "Fletre"}, ["Godewaersvelde"] = { latitude = 50.794256, longitude = 2.642912, display_name = "Godewaersvelde"}, ["Hesdigneul-les-Boulogne"] = { latitude = 50.659243, longitude = 1.672432, display_name = "Hesdigneul-les-Boulogne"}, ["Saint-Inglevert"] = { latitude = 50.875508, longitude = 1.742566, display_name = "Saint-Inglevert"}, ["Febvin-Palfart"] = { latitude = 50.538485, longitude = 2.315797, display_name = "Febvin-Palfart"}, ["Laires"] = { latitude = 50.539137, longitude = 2.255314, display_name = "Laires"}, ["Beaumetz-les-Aire"] = { latitude = 50.542628, longitude = 2.224930, display_name = "Beaumetz-les-Aire"}, ["Uxem"] = { latitude = 51.020525, longitude = 2.485008, display_name = "Uxem"}, ["Le Doulieu"] = { latitude = 50.682338, longitude = 2.718519, display_name = "Le Doulieu"}, ["Neuf-Berquin"] = { latitude = 50.660048, longitude = 2.670113, display_name = "Neuf-Berquin"}, ["Haverskerque"] = { latitude = 50.640764, longitude = 2.541629, display_name = "Haverskerque"}, ["Merris"] = { latitude = 50.715997, longitude = 2.661152, display_name = "Merris"}, ["Saint-Jans-Cappel"] = { latitude = 50.763955, longitude = 2.721262, display_name = "Saint-Jans-Cappel"}, ["Berthen"] = { latitude = 50.783094, longitude = 2.694833, display_name = "Berthen"}, ["Strazeele"] = { latitude = 50.726931, longitude = 2.632214, display_name = "Strazeele"}, ["Pradelles"] = { latitude = 50.731925, longitude = 2.604972, display_name = "Pradelles"}, ["Borre"] = { latitude = 50.731291, longitude = 2.584293, display_name = "Borre"}, ["Caestre"] = { latitude = 50.758358, longitude = 2.603998, display_name = "Caestre"}, ["Hondeghem"] = { latitude = 50.756499, longitude = 2.521257, display_name = "Hondeghem"}, ["Thiennes"] = { latitude = 50.651121, longitude = 2.465992, display_name = "Thiennes"}, ["Boeseghem"] = { latitude = 50.662021, longitude = 2.436942, display_name = "Boeseghem"}, ["Steenbecque"] = { latitude = 50.673869, longitude = 2.484934, display_name = "Steenbecque"}, ["Wallon-Cappel"] = { latitude = 50.727114, longitude = 2.474017, display_name = "Wallon-Cappel"}, ["Boeschepe"] = { latitude = 50.800274, longitude = 2.691633, display_name = "Boeschepe"}, ["Blaringhem"] = { latitude = 50.691533, longitude = 2.404284, display_name = "Blaringhem"}, ["Sercus"] = { latitude = 50.706649, longitude = 2.456155, display_name = "Sercus"}, ["Courset"] = { latitude = 50.647060, longitude = 1.840437, display_name = "Courset"}, ["Clairmarais"] = { latitude = 50.769714, longitude = 2.303873, display_name = "Clairmarais"}, ["Lynde"] = { latitude = 50.712720, longitude = 2.419570, display_name = "Lynde"}, ["Ebblinghem"] = { latitude = 50.733310, longitude = 2.409402, display_name = "Ebblinghem"}, ["Staple"] = { latitude = 50.749534, longitude = 2.454854, display_name = "Staple"}, ["Bavinchove"] = { latitude = 50.786146, longitude = 2.455895, display_name = "Bavinchove"}, ["Oxelaere"] = { latitude = 50.788573, longitude = 2.476408, display_name = "Oxelaere"}, ["Saint-Sylvestre-Cappel"] = { latitude = 50.776539, longitude = 2.554424, display_name = "Saint-Sylvestre-Cappel"}, ["Sainte-Marie-Cappel"] = { latitude = 50.783643, longitude = 2.510021, display_name = "Sainte-Marie-Cappel"}, ["Eecke"] = { latitude = 50.779085, longitude = 2.596637, display_name = "Eecke"}, ["Terdeghem"] = { latitude = 50.798750, longitude = 2.540184, display_name = "Terdeghem"}, ["Wissant"] = { latitude = 50.885779, longitude = 1.663546, display_name = "Wissant"}, ["Hardifort"] = { latitude = 50.820723, longitude = 2.485908, display_name = "Hardifort"}, ["Oudezeele"] = { latitude = 50.838533, longitude = 2.510881, display_name = "Oudezeele"}, ["Zuytpeene"] = { latitude = 50.793706, longitude = 2.430557, display_name = "Zuytpeene"}, ["Noordpeene"] = { latitude = 50.805883, longitude = 2.396303, display_name = "Noordpeene"}, ["Nieurlet"] = { latitude = 50.788599, longitude = 2.282161, display_name = "Nieurlet"}, ["Saint-Momelin"] = { latitude = 50.794897, longitude = 2.250181, display_name = "Saint-Momelin"}, ["Buysscheure"] = { latitude = 50.803823, longitude = 2.333426, display_name = "Buysscheure"}, ["Lederzeele"] = { latitude = 50.819633, longitude = 2.303059, display_name = "Lederzeele"}, ["Broxeele"] = { latitude = 50.830041, longitude = 2.319521, display_name = "Broxeele"}, ["Rubrouck"] = { latitude = 50.838312, longitude = 2.353890, display_name = "Rubrouck"}, ["Ochtezeele"] = { latitude = 50.817454, longitude = 2.400885, display_name = "Ochtezeele"}, ["Wemaers-Cappel"] = { latitude = 50.806728, longitude = 2.440173, display_name = "Wemaers-Cappel"}, ["Arneke"] = { latitude = 50.832041, longitude = 2.411757, display_name = "Arneke"}, ["Frethun"] = { latitude = 50.919591, longitude = 1.825603, display_name = "Frethun"}, ["Zermezeele"] = { latitude = 50.823506, longitude = 2.450104, display_name = "Zermezeele"}, ["Wulverdinghe"] = { latitude = 50.831729, longitude = 2.255670, display_name = "Wulverdinghe"}, ["Volckerinckhove"] = { latitude = 50.838367, longitude = 2.305745, display_name = "Volckerinckhove"}, ["Bollezeele"] = { latitude = 50.865595, longitude = 2.327453, display_name = "Bollezeele"}, ["Ledringhem"] = { latitude = 50.854568, longitude = 2.439630, display_name = "Ledringhem"}, ["Holque"] = { latitude = 50.853697, longitude = 2.203616, display_name = "Holque"}, ["Millam"] = { latitude = 50.854929, longitude = 2.248551, display_name = "Millam"}, ["Merckeghem"] = { latitude = 50.861588, longitude = 2.295689, display_name = "Merckeghem"}, ["Saint-Pierre-Brouck"] = { latitude = 50.896030, longitude = 2.189389, display_name = "Saint-Pierre-Brouck"}, ["Cappelle-Brouck"] = { latitude = 50.901860, longitude = 2.223000, display_name = "Cappelle-Brouck"}, ["Eringhem"] = { latitude = 50.896922, longitude = 2.344936, display_name = "Eringhem"}, ["Zegerscappel"] = { latitude = 50.887716, longitude = 2.385718, display_name = "Zegerscappel"}, ["Houtkerque"] = { latitude = 50.876696, longitude = 2.595551, display_name = "Houtkerque"}, ["Bambecque"] = { latitude = 50.901262, longitude = 2.547679, display_name = "Bambecque"}, ["Drincham"] = { latitude = 50.906747, longitude = 2.310390, display_name = "Drincham"}, ["Bissezeele"] = { latitude = 50.911634, longitude = 2.408987, display_name = "Bissezeele"}, ["Crochte"] = { latitude = 50.935284, longitude = 2.386984, display_name = "Crochte"}, ["Pitgam"] = { latitude = 50.927799, longitude = 2.330493, display_name = "Pitgam"}, ["Looberghe"] = { latitude = 50.916504, longitude = 2.272060, display_name = "Looberghe"}, ["Steene"] = { latitude = 50.952225, longitude = 2.369409, display_name = "Steene"}, ["West-Cappel"] = { latitude = 50.928273, longitude = 2.507585, display_name = "West-Cappel"}, ["Socx"] = { latitude = 50.936013, longitude = 2.422840, display_name = "Socx"}, ["Quaedypre"] = { latitude = 50.935395, longitude = 2.455633, display_name = "Quaedypre"}, ["Bierne"] = { latitude = 50.963476, longitude = 2.409818, display_name = "Bierne"}, ["Oost-Cappel"] = { latitude = 50.924611, longitude = 2.597743, display_name = "Oost-Cappel"}, ["Rexpoede"] = { latitude = 50.939354, longitude = 2.538858, display_name = "Rexpoede"}, ["Warhem"] = { latitude = 50.976760, longitude = 2.492958, display_name = "Warhem"}, ["Brouckerque"] = { latitude = 50.954119, longitude = 2.292949, display_name = "Brouckerque"}, ["Spycker"] = { latitude = 50.968436, longitude = 2.322961, display_name = "Spycker"}, ["Saint-Georges-sur-l'Aa"] = { latitude = 50.970102, longitude = 2.164564, display_name = "Saint-Georges-sur-l'Aa"}, ["Craywick"] = { latitude = 50.970686, longitude = 2.235816, display_name = "Craywick"}, ["Coudekerque-Village"] = { latitude = 50.994443, longitude = 2.417163, display_name = "Coudekerque-Village"}, ["Zuydcoote"] = { latitude = 51.063909, longitude = 2.490711, display_name = "Zuydcoote"}, ["Hardinghen"] = { latitude = 50.805869, longitude = 1.821639, display_name = "Hardinghen"}, ["Boursin"] = { latitude = 50.776524, longitude = 1.834829, display_name = "Boursin"}, ["Colembert"] = { latitude = 50.747612, longitude = 1.838257, display_name = "Colembert"}, ["Le Wast"] = { latitude = 50.750291, longitude = 1.802118, display_name = "Le Wast"}, ["Wierre-Effroy"] = { latitude = 50.779597, longitude = 1.738891, display_name = "Wierre-Effroy"}, ["Belle-et-Houllefort"] = { latitude = 50.744787, longitude = 1.760298, display_name = "Belle-et-Houllefort"}, ["Cremarest"] = { latitude = 50.701073, longitude = 1.785933, display_name = "Cremarest"}, ["Bellebrune"] = { latitude = 50.726348, longitude = 1.774510, display_name = "Bellebrune"}, ["Pittefaux"] = { latitude = 50.756594, longitude = 1.684987, display_name = "Pittefaux"}, ["Pernes-les-Boulogne"] = { latitude = 50.752184, longitude = 1.701034, display_name = "Pernes-les-Boulogne"}, ["Offrethun"] = { latitude = 50.783693, longitude = 1.693148, display_name = "Offrethun"}, ["Wacquinghen"] = { latitude = 50.783066, longitude = 1.667264, display_name = "Wacquinghen"}, ["Audresselles"] = { latitude = 50.824181, longitude = 1.595736, display_name = "Audresselles"}, ["Audinghen"] = { latitude = 50.852665, longitude = 1.614320, display_name = "Audinghen"}, ["Tardinghen"] = { latitude = 50.866592, longitude = 1.631452, display_name = "Tardinghen"}, ["Bazinghen"] = { latitude = 50.825670, longitude = 1.662221, display_name = "Bazinghen"}, ["Leulinghen-Bernes"] = { latitude = 50.832894, longitude = 1.712123, display_name = "Leulinghen-Bernes"}, ["Audembert"] = { latitude = 50.861176, longitude = 1.694268, display_name = "Audembert"}, ["Leubringhen"] = { latitude = 50.858229, longitude = 1.720168, display_name = "Leubringhen"}, ["Hervelinghen"] = { latitude = 50.881809, longitude = 1.713988, display_name = "Hervelinghen"}, ["Pihen-les-Guines"] = { latitude = 50.870387, longitude = 1.787442, display_name = "Pihen-les-Guines"}, ["Landrethun-le-Nord"] = { latitude = 50.847112, longitude = 1.782324, display_name = "Landrethun-le-Nord"}, ["Ferques"] = { latitude = 50.831049, longitude = 1.767750, display_name = "Ferques"}, ["Caffiers"] = { latitude = 50.841202, longitude = 1.809751, display_name = "Caffiers"}, ["Fiennes"] = { latitude = 50.828352, longitude = 1.827018, display_name = "Fiennes"}, ["Doudeauville"] = { latitude = 50.614815, longitude = 1.835048, display_name = "Doudeauville"}, ["Wirwignes"] = { latitude = 50.683400, longitude = 1.762810, display_name = "Wirwignes"}, ["Bonningues-les-Calais"] = { latitude = 50.890385, longitude = 1.774088, display_name = "Bonningues-les-Calais"}, ["Hucqueliers"] = { latitude = 50.569576, longitude = 1.907205, display_name = "Hucqueliers"}, ["Bourthes"] = { latitude = 50.606253, longitude = 1.932188, display_name = "Bourthes"}, ["Brunembert"] = { latitude = 50.713746, longitude = 1.893926, display_name = "Brunembert"}, ["Selles"] = { latitude = 50.700539, longitude = 1.896464, display_name = "Selles"}, ["Dannes"] = { latitude = 50.588960, longitude = 1.616037, display_name = "Dannes"}, ["Wardrecques"] = { latitude = 50.708784, longitude = 2.345237, display_name = "Wardrecques"}, ["Quiestede"] = { latitude = 50.679231, longitude = 2.316379, display_name = "Quiestede"}, ["Roquetoire"] = { latitude = 50.668017, longitude = 2.337788, display_name = "Roquetoire"}, ["Wittes"] = { latitude = 50.669305, longitude = 2.389990, display_name = "Wittes"}, ["Tatinghem"] = { latitude = 50.742310, longitude = 2.206800, display_name = "Tatinghem"}, ["Wisques"] = { latitude = 50.722827, longitude = 2.192666, display_name = "Wisques"}, ["Hallines"] = { latitude = 50.709050, longitude = 2.209680, display_name = "Hallines"}, ["Helfaut"] = { latitude = 50.696726, longitude = 2.240340, display_name = "Helfaut"}, ["Echinghen"] = { latitude = 50.703418, longitude = 1.647763, display_name = "Echinghen"}, ["Baincthun"] = { latitude = 50.710464, longitude = 1.680928, display_name = "Baincthun"}, ["Isques"] = { latitude = 50.682451, longitude = 1.644526, display_name = "Isques"}, ["Alincthun"] = { latitude = 50.731247, longitude = 1.803067, display_name = "Alincthun"}, ["Henneveux"] = { latitude = 50.723805, longitude = 1.851491, display_name = "Henneveux"}, ["Menneville"] = { latitude = 50.675532, longitude = 1.861136, display_name = "Menneville"}, ["Saint-Martin-Choquel"] = { latitude = 50.671384, longitude = 1.880557, display_name = "Saint-Martin-Choquel"}, ["Senlecques"] = { latitude = 50.648719, longitude = 1.937276, display_name = "Senlecques"}, ["Lottinghen"] = { latitude = 50.683841, longitude = 1.932854, display_name = "Lottinghen"}, ["Quesques"] = { latitude = 50.704463, longitude = 1.934006, display_name = "Quesques"}, ["Longueville"] = { latitude = 50.731896, longitude = 1.879146, display_name = "Longueville"}, ["Nabringhen"] = { latitude = 50.743744, longitude = 1.863321, display_name = "Nabringhen"}, ["Bournonville"] = { latitude = 50.706454, longitude = 1.850190, display_name = "Bournonville"}, ["Bainghen"] = { latitude = 50.752865, longitude = 1.907283, display_name = "Bainghen"}, ["Saint-Omer-Capelle"] = { latitude = 50.939181, longitude = 2.104024, display_name = "Saint-Omer-Capelle"}, ["Vieille-Eglise"] = { latitude = 50.928821, longitude = 2.078158, display_name = "Vieille-Eglise"}, ["Nouvelle-Eglise"] = { latitude = 50.924954, longitude = 2.053725, display_name = "Nouvelle-Eglise"}, ["Offekerque"] = { latitude = 50.941212, longitude = 2.019745, display_name = "Offekerque"}, ["Guemps"] = { latitude = 50.916922, longitude = 1.996767, display_name = "Guemps"}, ["Nortkerque"] = { latitude = 50.876134, longitude = 2.025252, display_name = "Nortkerque"}, ["Sainte-Marie-Kerque"] = { latitude = 50.900592, longitude = 2.137458, display_name = "Sainte-Marie-Kerque"}, ["Zutkerque"] = { latitude = 50.853410, longitude = 2.067984, display_name = "Zutkerque"}, ["Polincove"] = { latitude = 50.854509, longitude = 2.102259, display_name = "Polincove"}, ["Ruminghem"] = { latitude = 50.860075, longitude = 2.157454, display_name = "Ruminghem"}, ["Recques-sur-Hem"] = { latitude = 50.836537, longitude = 2.088250, display_name = "Recques-sur-Hem"}, ["Muncq-Nieurlet"] = { latitude = 50.848774, longitude = 2.115295, display_name = "Muncq-Nieurlet"}, ["Houlle"] = { latitude = 50.797741, longitude = 2.172855, display_name = "Houlle"}, ["Moulle"] = { latitude = 50.788109, longitude = 2.175840, display_name = "Moulle"}, ["Serques"] = { latitude = 50.793517, longitude = 2.202695, display_name = "Serques"}, ["Tilques"] = { latitude = 50.777735, longitude = 2.205176, display_name = "Tilques"}, ["Campagne-les-Boulonnais"] = { latitude = 50.613667, longitude = 1.996914, display_name = "Campagne-les-Boulonnais"}, ["Coyecques"] = { latitude = 50.603247, longitude = 2.183082, display_name = "Coyecques"}, ["Remilly-Wirquin"] = { latitude = 50.667494, longitude = 2.164347, display_name = "Remilly-Wirquin"}, ["Acquin-Westbecourt"] = { latitude = 50.728756, longitude = 2.089946, display_name = "Acquin-Westbecourt"}, ["Quercamps"] = { latitude = 50.753074, longitude = 2.051011, display_name = "Quercamps"}, ["Bomy"] = { latitude = 50.572070, longitude = 2.234239, display_name = "Bomy"}, ["Tournehem-sur-la-Hem"] = { latitude = 50.804719, longitude = 2.044890, display_name = "Tournehem-sur-la-Hem"}, ["Coulomby"] = { latitude = 50.706189, longitude = 2.008824, display_name = "Coulomby"}, ["Conteville-les-Boulogne"] = { latitude = 50.745072, longitude = 1.730434, display_name = "Conteville-les-Boulogne"}, ["Widehem"] = { latitude = 50.585707, longitude = 1.672850, display_name = "Widehem"}, ["Halinghen"] = { latitude = 50.602146, longitude = 1.691053, display_name = "Halinghen"}, ["Carly"] = { latitude = 50.652045, longitude = 1.701175, display_name = "Carly"}, ["Questrecques"] = { latitude = 50.664563, longitude = 1.746874, display_name = "Questrecques"}, ["Hesdin-l'Abbe"] = { latitude = 50.667044, longitude = 1.680201, display_name = "Hesdin-l'Abbe"}, ["Tubeauville"] = { latitude = 50.589381, longitude = 1.785342, display_name = "Tubeauville"}, ["Parenty"] = { latitude = 50.588629, longitude = 1.810162, display_name = "Parenty"}, ["Rety"] = { latitude = 50.796762, longitude = 1.774046, display_name = "Rety"}, ["Le Bois-Julien"] = { latitude = 50.637308, longitude = 1.815147, display_name = "Le Bois-Julien"}, ["Zouafques"] = { latitude = 50.816142, longitude = 2.054475, display_name = "Zouafques"}, ["La Capelle-les-Boulogne"] = { latitude = 50.731180, longitude = 1.714619, display_name = "La Capelle-les-Boulogne"}, ["Beuvrequen"] = { latitude = 50.801434, longitude = 1.665049, display_name = "Beuvrequen"}, ["Heuringhem"] = { latitude = 50.696059, longitude = 2.283668, display_name = "Heuringhem"}, ["Inghem"] = { latitude = 50.668344, longitude = 2.243240, display_name = "Inghem"}, ["Therouanne"] = { latitude = 50.636334, longitude = 2.257719, display_name = "Therouanne"}, ["Herbelles"] = { latitude = 50.655227, longitude = 2.222924, display_name = "Herbelles"}, ["Pollinkhove"] = { latitude = 50.969266, longitude = 2.730865, display_name = "Pollinkhove"}, ["Proven"] = { latitude = 50.890344, longitude = 2.654287, display_name = "Proven"}, ["Roesbrugge-Haringe"] = { latitude = 50.915053, longitude = 2.626259, display_name = "Roesbrugge-Haringe"}, ["Westvleteren"] = { latitude = 50.927286, longitude = 2.717444, display_name = "Westvleteren"}, ["Oostvleteren"] = { latitude = 50.933701, longitude = 2.741004, display_name = "Oostvleteren"}, ["Adinkerke"] = { latitude = 51.076216, longitude = 2.598567, display_name = "Adinkerke"}, ["Avekapelle"] = { latitude = 51.066764, longitude = 2.733326, display_name = "Avekapelle"}, ["Oostduinkerke"] = { latitude = 51.115634, longitude = 2.681266, display_name = "Oostduinkerke"}, ["Sangatte"] = { latitude = 50.945804, longitude = 1.753713, display_name = "Sangatte"}, ["Alquines"] = { latitude = 50.740526, longitude = 1.992636, display_name = "Alquines"}, ["Amettes"] = { latitude = 50.531129, longitude = 2.394840, display_name = "Amettes"}, ["Andres"] = { latitude = 50.863969, longitude = 1.914154, display_name = "Andres"}, ["Auchy-au-Bois"] = { latitude = 50.555179, longitude = 2.371047, display_name = "Auchy-au-Bois"}, ["Audincthun"] = { latitude = 50.582893, longitude = 2.143174, display_name = "Audincthun"}, ["Avroult"] = { latitude = 50.633519, longitude = 2.146319, display_name = "Avroult"}, ["Busnes"] = { latitude = 50.587892, longitude = 2.518470, display_name = "Busnes"}, ["Witternesse"] = { latitude = 50.610836, longitude = 2.359894, display_name = "Witternesse"}, ["Blessy"] = { latitude = 50.616305, longitude = 2.331492, display_name = "Blessy"}, ["Bourecq"] = { latitude = 50.571648, longitude = 2.433932, display_name = "Bourecq"}, ["Calonne-sur-la-Lys"] = { latitude = 50.623651, longitude = 2.615153, display_name = "Calonne-sur-la-Lys"}, ["Clety"] = { latitude = 50.651854, longitude = 2.181948, display_name = "Clety"}, ["Dennebroeucq"] = { latitude = 50.572489, longitude = 2.152864, display_name = "Dennebroeucq"}, ["Ecquedecques"] = { latitude = 50.562119, longitude = 2.448703, display_name = "Ecquedecques"}, ["Enguinegatte"] = { latitude = 50.607409, longitude = 2.271558, display_name = "Enguinegatte"}, ["Enquin-les-Mines"] = { latitude = 50.588395, longitude = 2.286359, display_name = "Enquin-les-Mines"}, ["Erny-Saint-Julien"] = { latitude = 50.585429, longitude = 2.253229, display_name = "Erny-Saint-Julien"}, ["Essars"] = { latitude = 50.547241, longitude = 2.664094, display_name = "Essars"}, ["Estree-Blanche"] = { latitude = 50.592872, longitude = 2.321795, display_name = "Estree-Blanche"}, ["Festubert"] = { latitude = 50.542610, longitude = 2.736511, display_name = "Festubert"}, ["Flechin"] = { latitude = 50.558738, longitude = 2.292781, display_name = "Flechin"}, ["Guarbecque"] = { latitude = 50.609751, longitude = 2.488475, display_name = "Guarbecque"}, ["Hames-Boucres"] = { latitude = 50.880241, longitude = 1.841748, display_name = "Hames-Boucres"}, ["Ham-en-Artois"] = { latitude = 50.590203, longitude = 2.454749, display_name = "Ham-en-Artois"}, ["Hinges"] = { latitude = 50.565461, longitude = 2.621913, display_name = "Hinges"}, ["Le Maisnil-Boutry"] = { latitude = 50.644241, longitude = 2.002657, display_name = "Le Maisnil-Boutry"}, ["Drionville"] = { latitude = 50.645111, longitude = 2.053126, display_name = "Drionville"}, ["Lambres"] = { latitude = 50.617833, longitude = 2.395999, display_name = "Lambres"}, ["Ledinghem"] = { latitude = 50.652450, longitude = 1.988667, display_name = "Ledinghem"}, ["Liettres"] = { latitude = 50.595750, longitude = 2.341847, display_name = "Liettres"}, ["Ligny-les-Aire"] = { latitude = 50.557335, longitude = 2.349206, display_name = "Ligny-les-Aire"}, ["Linghem"] = { latitude = 50.595558, longitude = 2.371814, display_name = "Linghem"}, ["Westrehem"] = { latitude = 50.545780, longitude = 2.345838, display_name = "Westrehem"}, ["Mont-Bernanchon"] = { latitude = 50.593087, longitude = 2.602329, display_name = "Mont-Bernanchon"}, ["Norrent-Fontes"] = { latitude = 50.585552, longitude = 2.410162, display_name = "Norrent-Fontes"}, ["Merck-Saint-Lievin"] = { latitude = 50.627330, longitude = 2.114108, display_name = "Merck-Saint-Lievin"}, ["Matringhem"] = { latitude = 50.545919, longitude = 2.165546, display_name = "Matringhem"}, ["Quernes"] = { latitude = 50.605240, longitude = 2.363373, display_name = "Quernes"}, ["Robecq"] = { latitude = 50.595018, longitude = 2.563650, display_name = "Robecq"}, ["Saint-Floris"] = { latitude = 50.627363, longitude = 2.570130, display_name = "Saint-Floris"}, ["Vieille-Chapelle"] = { latitude = 50.590701, longitude = 2.703245, display_name = "Vieille-Chapelle"}, ["Renty"] = { latitude = 50.580661, longitude = 2.073618, display_name = "Renty"}, ["Radinghem"] = { latitude = 50.553405, longitude = 2.122892, display_name = "Radinghem"}, ["Rely"] = { latitude = 50.572872, longitude = 2.360502, display_name = "Rely"}, ["Saint-Hilaire-Cottes"] = { latitude = 50.570344, longitude = 2.414031, display_name = "Saint-Hilaire-Cottes"}, ["Senlis"] = { latitude = 50.533161, longitude = 2.152329, display_name = "Senlis"}, ["Ecques"] = { latitude = 50.669543, longitude = 2.286275, display_name = "Ecques"}, ["Clarques"] = { latitude = 50.646610, longitude = 2.276508, display_name = "Clarques"}, ["Nielles-les-Blequin"] = { latitude = 50.673466, longitude = 2.030379, display_name = "Nielles-les-Blequin"}, ["Delettes"] = { latitude = 50.618732, longitude = 2.213249, display_name = "Delettes"}, ["Dohem"] = { latitude = 50.637767, longitude = 2.186722, display_name = "Dohem"}, ["Setques"] = { latitude = 50.712219, longitude = 2.157863, display_name = "Setques"}, ["Sainte-Cecile-Plage"] = { latitude = 50.574716, longitude = 1.582627, display_name = "Sainte-Cecile-Plage"}, ["Inxent"] = { latitude = 50.535681, longitude = 1.784042, display_name = "Inxent"}, ["Campagne-les-Guines"] = { latitude = 50.841095, longitude = 1.902068, display_name = "Campagne-les-Guines"}, ["Peuplingues"] = { latitude = 50.915136, longitude = 1.769888, display_name = "Peuplingues"}, ["Framezelle"] = { latitude = 50.863571, longitude = 1.592289, display_name = "Framezelle"}, ["Wormhout"] = { latitude = 50.882763, longitude = 2.468733, display_name = "Wormhout"}, ["Herzeele"] = { latitude = 50.886001, longitude = 2.531993, display_name = "Herzeele"}, ["Le Grand Millebrugghe"] = { latitude = 50.965001, longitude = 2.347062, display_name = "Le Grand Millebrugghe"}, ["Veurne"] = { latitude = 51.072427, longitude = 2.662132, display_name = "Veurne"}, ["Moringhem"] = { latitude = 50.763120, longitude = 2.127110, display_name = "Moringhem"}, ["Quelmes"] = { latitude = 50.732691, longitude = 2.136718, display_name = "Quelmes"}, ["Leulinghem"] = { latitude = 50.734748, longitude = 2.164308, display_name = "Leulinghem"}, ["Affringues"] = { latitude = 50.689861, longitude = 2.074325, display_name = "Affringues"}, ["Bayenghem-les-Seninghem"] = { latitude = 50.699930, longitude = 2.075930, display_name = "Bayenghem-les-Seninghem"}, ["Bernieulles"] = { latitude = 50.554600, longitude = 1.772850, display_name = "Bernieulles"}, ["Bezinghem"] = { latitude = 50.593710, longitude = 1.825480, display_name = "Bezinghem"}, ["Bimont"] = { latitude = 50.541460, longitude = 1.902770, display_name = "Bimont"}, ["Blequin"] = { latitude = 50.663360, longitude = 1.989060, display_name = "Blequin"}, ["Boisdinghem"] = { latitude = 50.748520, longitude = 2.093420, display_name = "Boisdinghem"}, ["Bouvelinghem"] = { latitude = 50.734246, longitude = 2.034073, display_name = "Bouvelinghem"}, ["Elnes"] = { latitude = 50.689520, longitude = 2.125570, display_name = "Elnes"}, ["Enquin-sur-Baillons"] = { latitude = 50.572260, longitude = 1.835850, display_name = "Enquin-sur-Baillons"}, ["Ergny"] = { latitude = 50.583410, longitude = 1.980800, display_name = "Ergny"}, ["Escoeuilles"] = { latitude = 50.725650, longitude = 1.925600, display_name = "Escoeuilles"}, ["Esquerdes"] = { latitude = 50.705520, longitude = 2.182975, display_name = "Esquerdes"}, ["Haut-Loquin"] = { latitude = 50.739450, longitude = 1.966310, display_name = "Haut-Loquin"}, ["Herly"] = { latitude = 50.546650, longitude = 1.985770, display_name = "Herly"}, ["Mametz"] = { latitude = 50.634220, longitude = 2.324090, display_name = "Mametz"}, ["Maninghem"] = { latitude = 50.542670, longitude = 1.939360, display_name = "Maninghem"}, ["Ouve-Wirquin"] = { latitude = 50.650160, longitude = 2.143190, display_name = "Ouve-Wirquin"}, ["Preures"] = { latitude = 50.571740, longitude = 1.876730, display_name = "Preures"}, ["Rebecques"] = { latitude = 50.644890, longitude = 2.305600, display_name = "Rebecques"}, ["Rumilly"] = { latitude = 50.576510, longitude = 2.014770, display_name = "Rumilly"}, ["Seninghem"] = { latitude = 50.702354, longitude = 2.036721, display_name = "Seninghem"}, ["Surques"] = { latitude = 50.739934, longitude = 1.916742, display_name = "Surques"}, ["Verchocq"] = { latitude = 50.564189, longitude = 2.036242, display_name = "Verchocq"}, ["Wavrans-sur-l'Aa"] = { latitude = 50.682410, longitude = 2.135580, display_name = "Wavrans-sur-l'Aa"}, ["Wicquinghem"] = { latitude = 50.575170, longitude = 1.961230, display_name = "Wicquinghem"}, ["Wismes"] = { latitude = 50.653230, longitude = 2.071140, display_name = "Wismes"}, ["Zoteux"] = { latitude = 50.610360, longitude = 1.878690, display_name = "Zoteux"}, ["Les Attaques"] = { latitude = 50.907231, longitude = 1.933694, display_name = "Les Attaques"}, ["Nielles-les-Calais"] = { latitude = 50.907587, longitude = 1.829358, display_name = "Nielles-les-Calais"}, ["Maninghen-Henne"] = { latitude = 50.768253, longitude = 1.668139, display_name = "Maninghen-Henne"}, ["Hermelinghen"] = { latitude = 50.802584, longitude = 1.858957, display_name = "Hermelinghen"}, ["Bouquehault"] = { latitude = 50.826595, longitude = 1.902510, display_name = "Bouquehault"}, ["Alembon"] = { latitude = 50.784734, longitude = 1.887005, display_name = "Alembon"}, ["Sanghen"] = { latitude = 50.776102, longitude = 1.900075, display_name = "Sanghen"}, ["Balinghem"] = { latitude = 50.860659, longitude = 1.943506, display_name = "Balinghem"}, ["Bremes"] = { latitude = 50.857017, longitude = 1.961340, display_name = "Bremes"}, ["Rodelinghem"] = { latitude = 50.838050, longitude = 1.928728, display_name = "Rodelinghem"}, ["Herbinghen"] = { latitude = 50.772682, longitude = 1.912526, display_name = "Herbinghen"}, ["Licques"] = { latitude = 50.784662, longitude = 1.931790, display_name = "Licques"}, ["Nielles-les-Ardres"] = { latitude = 50.842190, longitude = 2.016594, display_name = "Nielles-les-Ardres"}, ["Autingues"] = { latitude = 50.842121, longitude = 1.985072, display_name = "Autingues"}, ["Louches"] = { latitude = 50.829819, longitude = 2.004976, display_name = "Louches"}, ["Hocquinghen"] = { latitude = 50.769037, longitude = 1.937095, display_name = "Hocquinghen"}, ["Clerques"] = { latitude = 50.793353, longitude = 1.993673, display_name = "Clerques"}, ["Audrehem"] = { latitude = 50.781378, longitude = 1.989835, display_name = "Audrehem"}, ["Bonningues-les-Ardres"] = { latitude = 50.791896, longitude = 2.013201, display_name = "Bonningues-les-Ardres"}, ["Rebergues"] = { latitude = 50.754289, longitude = 1.958913, display_name = "Rebergues"}, ["Journy"] = { latitude = 50.753379, longitude = 1.994703, display_name = "Journy"}, ["Nordausques"] = { latitude = 50.818666, longitude = 2.079704, display_name = "Nordausques"}, ["Nort-Leulinghem"] = { latitude = 50.801674, longitude = 2.092434, display_name = "Nort-Leulinghem"}, ["Bayenghem-les-Eperlecques"] = { latitude = 50.806406, longitude = 2.125144, display_name = "Bayenghem-les-Eperlecques"}, ["Mentque-Nortbecourt"] = { latitude = 50.769882, longitude = 2.090772, display_name = "Mentque-Nortbecourt"}, ["Zudausques"] = { latitude = 50.749725, longitude = 2.149092, display_name = "Zudausques"}, ["Vaudringhem"] = { latitude = 50.661744, longitude = 2.029476, display_name = "Vaudringhem"}, ["Campagne-les-Wardrecques"] = { latitude = 50.717803, longitude = 2.333551, display_name = "Campagne-les-Wardrecques"}, ["Longfosse"] = { latitude = 50.651674, longitude = 1.805305, display_name = "Longfosse"}, ["Wierre-au-Bois"] = { latitude = 50.645164, longitude = 1.762960, display_name = "Wierre-au-Bois"}, ["Tingry"] = { latitude = 50.618031, longitude = 1.730393, display_name = "Tingry"}, ["Lacres"] = { latitude = 50.600977, longitude = 1.761913, display_name = "Lacres"}, ["Becourt"] = { latitude = 50.638133, longitude = 1.910800, display_name = "Becourt"}, ["Hubersent"] = { latitude = 50.581108, longitude = 1.726437, display_name = "Hubersent"}, ["Frencq"] = { latitude = 50.561810, longitude = 1.699536, display_name = "Frencq"}, ["Cormont"] = { latitude = 50.561374, longitude = 1.735293, display_name = "Cormont"}, ["Lefaux"] = { latitude = 50.542416, longitude = 1.659116, display_name = "Lefaux"}, ["Longvilliers"] = { latitude = 50.543611, longitude = 1.727947, display_name = "Longvilliers"}, ["Beussent"] = { latitude = 50.545930, longitude = 1.795064, display_name = "Beussent"}, ["Avesnes"] = { latitude = 50.550698, longitude = 1.969351, display_name = "Avesnes"}, ["Reclinghem"] = { latitude = 50.572213, longitude = 2.174423, display_name = "Reclinghem"}, ["Vincly"] = { latitude = 50.559224, longitude = 2.171241, display_name = "Vincly"}, ["Ames"] = { latitude = 50.536715, longitude = 2.408654, display_name = "Ames"}, ["Lespesses"] = { latitude = 50.563281, longitude = 2.422028, display_name = "Lespesses"}, ["Lieres"] = { latitude = 50.554453, longitude = 2.417038, display_name = "Lieres"}, ["Mazinghem"] = { latitude = 50.601999, longitude = 2.405251, display_name = "Mazinghem"}, ["Rombly"] = { latitude = 50.596850, longitude = 2.388703, display_name = "Rombly"}, ["Oblinghem"] = { latitude = 50.549723, longitude = 2.599434, display_name = "Oblinghem"}, ["Landrethun-les-Ardres"] = { latitude = 50.825020, longitude = 1.960463, display_name = "Landrethun-les-Ardres"}, ["Audenfort"] = { latitude = 50.783137, longitude = 1.970780, display_name = "Audenfort"}, ["Mentque"] = { latitude = 50.783977, longitude = 2.085649, display_name = "Mentque"}, ["Pihem"] = { latitude = 50.682756, longitude = 2.213203, display_name = "Pihem"}, ["Wandonne"] = { latitude = 50.564399, longitude = 2.126731, display_name = "Wandonne"}, ["Cuhem"] = { latitude = 50.569579, longitude = 2.277348, display_name = "Cuhem"}, ["Le Clivet"] = { latitude = 50.567150, longitude = 1.888315, display_name = "Le Clivet"}, ["Wimereux"] = { latitude = 50.769686, longitude = 1.611861, display_name = "Wimereux"}, ["Thiembronne"] = { latitude = 50.621035, longitude = 2.058926, display_name = "Thiembronne"}, ["Outtersteene"] = { latitude = 50.712098, longitude = 2.682043, display_name = "Outtersteene"}, ["Salperwick"] = { latitude = 50.771874, longitude = 2.230078, display_name = "Salperwick"}, ["Noeux-les-Mines"] = { latitude = 50.475400, longitude = 2.662190, display_name = "Noeux-les-Mines"}, ["Bethune"] = { latitude = 50.519900, longitude = 2.647810, display_name = "Bethune"}, ["Abbeville"] = { latitude = 50.106083, longitude = 1.833703, display_name = "Abbeville"}, ["Etaples"] = { latitude = 50.513955, longitude = 1.638625, display_name = "Etaples"}, ["Albert"] = { latitude = 50.001802, longitude = 2.650922, display_name = "Albert"}, ["Berck"] = { latitude = 50.405258, longitude = 1.571162, display_name = "Berck"}, ["Dieppe"] = { latitude = 49.924618, longitude = 1.079144, display_name = "Dieppe"}, ["Bruay-la-Buissiere"] = { latitude = 50.482196, longitude = 2.546192, display_name = "Bruay-la-Buissiere"}, ["Auchel"] = { latitude = 50.503749, longitude = 2.468206, display_name = "Auchel"}, ["Gamaches"] = { latitude = 49.985523, longitude = 1.555718, display_name = "Gamaches"}, ["Avesnes-le-Comte"] = { latitude = 50.276202, longitude = 2.527700, display_name = "Avesnes-le-Comte"}, ["Blangy-sur-Bresle"] = { latitude = 49.932376, longitude = 1.629683, display_name = "Blangy-sur-Bresle"}, ["Flesselles"] = { latitude = 50.004900, longitude = 2.263360, display_name = "Flesselles"}, ["Hesdin"] = { latitude = 50.373000, longitude = 2.036614, display_name = "Hesdin"}, ["Doullens"] = { latitude = 50.157209, longitude = 2.341053, display_name = "Doullens"}, ["Saint-Valery-sur-Somme"] = { latitude = 50.188701, longitude = 1.627915, display_name = "Saint-Valery-sur-Somme"}, ["Haillicourt"] = { latitude = 50.475100, longitude = 2.577930, display_name = "Haillicourt"}, ["Beauval"] = { latitude = 50.106225, longitude = 2.331653, display_name = "Beauval"}, ["Mers-les-Bains"] = { latitude = 50.065632, longitude = 1.388970, display_name = "Mers-les-Bains"}, ["Feuquieres-en-Vimeu"] = { latitude = 50.060937, longitude = 1.609549, display_name = "Feuquieres-en-Vimeu"}, ["Barlin"] = { latitude = 50.456836, longitude = 2.617477, display_name = "Barlin"}, ["Fressenneville"] = { latitude = 50.068204, longitude = 1.575208, display_name = "Fressenneville"}, ["Ault"] = { latitude = 50.101564, longitude = 1.447301, display_name = "Ault"}, ["Hersin-Coupigny"] = { latitude = 50.447236, longitude = 2.648495, display_name = "Hersin-Coupigny"}, ["Merlimont"] = { latitude = 50.455808, longitude = 1.613436, display_name = "Merlimont"}, ["Cayeux-sur-Mer"] = { latitude = 50.179200, longitude = 1.493384, display_name = "Cayeux-sur-Mer"}, ["Saint-Ouen"] = { latitude = 50.036830, longitude = 2.122374, display_name = "Saint-Ouen"}, ["Frevent"] = { latitude = 50.278213, longitude = 2.292979, display_name = "Frevent"}, ["Verquin"] = { latitude = 50.501142, longitude = 2.641114, display_name = "Verquin"}, ["Auxi-le-Chateau"] = { latitude = 50.231491, longitude = 2.117761, display_name = "Auxi-le-Chateau"}, ["Friville-Escarbotin"] = { latitude = 50.086995, longitude = 1.545222, display_name = "Friville-Escarbotin"}, ["Divion"] = { latitude = 50.469400, longitude = 2.507220, display_name = "Divion"}, ["Le Touquet-Paris-Plage"] = { latitude = 50.521120, longitude = 1.590932, display_name = "Le Touquet-Paris-Plage"}, ["Saint-Pol-sur-Ternoise"] = { latitude = 50.381211, longitude = 2.336105, display_name = "Saint-Pol-sur-Ternoise"}, ["Lapugnoy"] = { latitude = 50.520400, longitude = 2.534200, display_name = "Lapugnoy"}, ["Flixecourt"] = { latitude = 50.011710, longitude = 2.082582, display_name = "Flixecourt"}, ["Rang-du-Fliers"] = { latitude = 50.416897, longitude = 1.643935, display_name = "Rang-du-Fliers"}, ["Criel-sur-Mer"] = { latitude = 50.016093, longitude = 1.313944, display_name = "Criel-sur-Mer"}, ["Eu"] = { latitude = 50.049170, longitude = 1.417574, display_name = "Eu"}, ["Ailly-sur-Somme"] = { latitude = 49.928085, longitude = 2.198100, display_name = "Ailly-sur-Somme"}, ["Labourse"] = { latitude = 50.497298, longitude = 2.679865, display_name = "Labourse"}, ["Montreuil"] = { latitude = 50.463892, longitude = 1.763113, display_name = "Montreuil"}, ["Marles-les-Mines"] = { latitude = 50.501949, longitude = 2.506041, display_name = "Marles-les-Mines"}, ["Cauchy-a-la-Tour"] = { latitude = 50.503104, longitude = 2.452112, display_name = "Cauchy-a-la-Tour"}, ["Bouvigny-Boyeffles"] = { latitude = 50.421195, longitude = 2.672681, display_name = "Bouvigny-Boyeffles"}, ["Fruges"] = { latitude = 50.515039, longitude = 2.134929, display_name = "Fruges"}, ["Sains-en-Gohelle"] = { latitude = 50.447328, longitude = 2.683388, display_name = "Sains-en-Gohelle"}, ["Airaines"] = { latitude = 49.966270, longitude = 1.941064, display_name = "Airaines"}, ["Wailly-Beaucamp"] = { latitude = 50.410866, longitude = 1.726233, display_name = "Wailly-Beaucamp"}, ["Azincourt"] = { latitude = 50.463383, longitude = 2.128827, display_name = "Azincourt"}, ["Tortefontaine"] = { latitude = 50.322728, longitude = 1.923292, display_name = "Tortefontaine"}, ["Saulchoy"] = { latitude = 50.349999, longitude = 1.850295, display_name = "Saulchoy"}, ["Douriez"] = { latitude = 50.332920, longitude = 1.877159, display_name = "Douriez"}, ["Maintenay"] = { latitude = 50.366386, longitude = 1.813077, display_name = "Maintenay"}, ["Roussent"] = { latitude = 50.368082, longitude = 1.777043, display_name = "Roussent"}, ["Nempont-Saint-Firmin"] = { latitude = 50.355394, longitude = 1.732144, display_name = "Nempont-Saint-Firmin"}, ["Gouy-Saint-Andre"] = { latitude = 50.373606, longitude = 1.900334, display_name = "Gouy-Saint-Andre"}, ["Mouriez"] = { latitude = 50.340945, longitude = 1.947197, display_name = "Mouriez"}, ["Bertangles"] = { latitude = 49.970940, longitude = 2.299291, display_name = "Bertangles"}, ["Villers-Bocage"] = { latitude = 49.995675, longitude = 2.314198, display_name = "Villers-Bocage"}, ["Molliens-au-Bois"] = { latitude = 49.989616, longitude = 2.385043, display_name = "Molliens-au-Bois"}, ["Montigny-sur-l'Hallue"] = { latitude = 49.980230, longitude = 2.443205, display_name = "Montigny-sur-l'Hallue"}, ["Behencourt"] = { latitude = 49.974472, longitude = 2.451315, display_name = "Behencourt"}, ["Pont-Noyelles"] = { latitude = 49.939781, longitude = 2.440894, display_name = "Pont-Noyelles"}, ["Querrieu"] = { latitude = 49.939113, longitude = 2.431445, display_name = "Querrieu"}, ["Enocq"] = { latitude = 50.498729, longitude = 1.703481, display_name = "Enocq"}, ["Lucheux"] = { latitude = 50.196945, longitude = 2.411534, display_name = "Lucheux"}, ["Cambligneul"] = { latitude = 50.379859, longitude = 2.615773, display_name = "Cambligneul"}, ["Lahoussoye"] = { latitude = 49.952856, longitude = 2.483167, display_name = "Lahoussoye"}, ["Bonnay"] = { latitude = 49.934650, longitude = 2.512065, display_name = "Bonnay"}, ["Picquigny"] = { latitude = 49.944326, longitude = 2.141807, display_name = "Picquigny"}, ["Hallencourt"] = { latitude = 49.992824, longitude = 1.876855, display_name = "Hallencourt"}, ["Lisbourg"] = { latitude = 50.507259, longitude = 2.216503, display_name = "Lisbourg"}, ["Verchin"] = { latitude = 50.494047, longitude = 2.184971, display_name = "Verchin"}, ["Lugy"] = { latitude = 50.523288, longitude = 2.173914, display_name = "Lugy"}, ["Heuchin"] = { latitude = 50.474619, longitude = 2.269143, display_name = "Heuchin"}, ["Bergueneuse"] = { latitude = 50.468606, longitude = 2.253501, display_name = "Bergueneuse"}, ["Anvin"] = { latitude = 50.446435, longitude = 2.252243, display_name = "Anvin"}, ["Monchy-Cayeux"] = { latitude = 50.436459, longitude = 2.278342, display_name = "Monchy-Cayeux"}, ["Wavrans-sur-Ternoise"] = { latitude = 50.414381, longitude = 2.299602, display_name = "Wavrans-sur-Ternoise"}, ["Hernicourt"] = { latitude = 50.407907, longitude = 2.304976, display_name = "Hernicourt"}, ["Gauchin-Verloingt"] = { latitude = 50.395392, longitude = 2.312410, display_name = "Gauchin-Verloingt"}, ["Fiefs"] = { latitude = 50.503443, longitude = 2.328885, display_name = "Fiefs"}, ["Tangry"] = { latitude = 50.465104, longitude = 2.354806, display_name = "Tangry"}, ["Valhuon"] = { latitude = 50.435419, longitude = 2.375445, display_name = "Valhuon"}, ["Brias"] = { latitude = 50.409802, longitude = 2.378303, display_name = "Brias"}, ["Pernes"] = { latitude = 50.484100, longitude = 2.410092, display_name = "Pernes"}, ["Fontaine-les-Boulans"] = { latitude = 50.498360, longitude = 2.275293, display_name = "Fontaine-les-Boulans"}, ["Naours"] = { latitude = 50.034144, longitude = 2.274822, display_name = "Naours"}, ["Ramecourt"] = { latitude = 50.370249, longitude = 2.314578, display_name = "Ramecourt"}, ["Le Parcq"] = { latitude = 50.379561, longitude = 2.099157, display_name = "Le Parcq"}, ["Huby-Saint-Leu"] = { latitude = 50.381317, longitude = 2.037182, display_name = "Huby-Saint-Leu"}, ["Marconne"] = { latitude = 50.370368, longitude = 2.045287, display_name = "Marconne"}, ["Regnauville"] = { latitude = 50.313621, longitude = 2.013962, display_name = "Regnauville"}, ["Le Boisle"] = { latitude = 50.271777, longitude = 1.985295, display_name = "Le Boisle"}, ["Froyelles"] = { latitude = 50.226877, longitude = 1.929308, display_name = "Froyelles"}, ["Drucat"] = { latitude = 50.141850, longitude = 1.868445, display_name = "Drucat"}, ["Grand-Laviers"] = { latitude = 50.128684, longitude = 1.785705, display_name = "Grand-Laviers"}, ["Ville-le-Marclet"] = { latitude = 50.022943, longitude = 2.088392, display_name = "Ville-le-Marclet"}, ["Saint-Leger-les-Domart"] = { latitude = 50.053579, longitude = 2.139729, display_name = "Saint-Leger-les-Domart"}, ["Domart-en-Ponthieu"] = { latitude = 50.074166, longitude = 2.126317, display_name = "Domart-en-Ponthieu"}, ["La Calotterie"] = { latitude = 50.475205, longitude = 1.726325, display_name = "La Calotterie"}, ["Dernancourt"] = { latitude = 49.973252, longitude = 2.631678, display_name = "Dernancourt"}, ["Buire-sur-l'Ancre"] = { latitude = 49.965182, longitude = 2.592539, display_name = "Buire-sur-l'Ancre"}, ["Ribemont-sur-Ancre"] = { latitude = 49.960156, longitude = 2.565479, display_name = "Ribemont-sur-Ancre"}, ["Mericourt-l'Abbe"] = { latitude = 49.952297, longitude = 2.563810, display_name = "Mericourt-l'Abbe"}, ["Ancourt"] = { latitude = 49.910934, longitude = 1.179063, display_name = "Ancourt"}, ["Arrest"] = { latitude = 50.126994, longitude = 1.616619, display_name = "Arrest"}, ["Rubempre"] = { latitude = 50.018360, longitude = 2.384620, display_name = "Rubempre"}, ["Estreboeuf"] = { latitude = 50.156111, longitude = 1.615395, display_name = "Estreboeuf"}, ["Boismont"] = { latitude = 50.153208, longitude = 1.684135, display_name = "Boismont"}, ["Crecy-en-Ponthieu"] = { latitude = 50.252146, longitude = 1.884066, display_name = "Crecy-en-Ponthieu"}, ["Nouvion"] = { latitude = 50.212477, longitude = 1.777915, display_name = "Nouvion"}, ["Saint-Martin-en-Campagne"] = { latitude = 49.956823, longitude = 1.221892, display_name = "Saint-Martin-en-Campagne"}, ["Grandcourt"] = { latitude = 49.913749, longitude = 1.491097, display_name = "Grandcourt"}, ["Le Treport"] = { latitude = 50.059110, longitude = 1.382766, display_name = "Le Treport"}, ["Le Crotoy"] = { latitude = 50.216569, longitude = 1.624047, display_name = "Le Crotoy"}, ["Estree"] = { latitude = 50.499322, longitude = 1.791475, display_name = "Estree"}, ["Montcavrel"] = { latitude = 50.514675, longitude = 1.810636, display_name = "Montcavrel"}, ["Marles-sur-Canche"] = { latitude = 50.458151, longitude = 1.826960, display_name = "Marles-sur-Canche"}, ["Saint-Josse"] = { latitude = 50.467477, longitude = 1.663984, display_name = "Saint-Josse"}, ["Saint-Aubin"] = { latitude = 50.457369, longitude = 1.665185, display_name = "Saint-Aubin"}, ["Sorrus"] = { latitude = 50.456844, longitude = 1.715139, display_name = "Sorrus"}, ["Brimeux"] = { latitude = 50.445700, longitude = 1.834628, display_name = "Brimeux"}, ["Campigneulles-les-Petites"] = { latitude = 50.444800, longitude = 1.733805, display_name = "Campigneulles-les-Petites"}, ["Bouzincourt"] = { latitude = 50.026323, longitude = 2.610171, display_name = "Bouzincourt"}, ["Vacqueriette-Erquieres"] = { latitude = 50.322689, longitude = 2.078418, display_name = "Vacqueriette-Erquieres"}, ["Miannay"] = { latitude = 50.097549, longitude = 1.717694, display_name = "Miannay"}, ["Valines"] = { latitude = 50.076011, longitude = 1.621392, display_name = "Valines"}, ["Epagne-Epagnette"] = { latitude = 50.073673, longitude = 1.870325, display_name = "Epagne-Epagnette"}, ["Eaucourt-sur-Somme"] = { latitude = 50.064496, longitude = 1.884941, display_name = "Eaucourt-sur-Somme"}, ["Ailly-le-Haut-Clocher"] = { latitude = 50.076495, longitude = 1.992357, display_name = "Ailly-le-Haut-Clocher"}, ["Pont-Remy"] = { latitude = 50.055774, longitude = 1.901735, display_name = "Pont-Remy"}, ["Canaples"] = { latitude = 50.056802, longitude = 2.218586, display_name = "Canaples"}, ["Havernas"] = { latitude = 50.036637, longitude = 2.233241, display_name = "Havernas"}, ["Halloy-les-Pernois"] = { latitude = 50.050258, longitude = 2.201617, display_name = "Halloy-les-Pernois"}, ["Berteaucourt-les-Dames"] = { latitude = 50.045899, longitude = 2.153166, display_name = "Berteaucourt-les-Dames"}, ["Woignarue"] = { latitude = 50.109473, longitude = 1.494677, display_name = "Woignarue"}, ["Ponthoile"] = { latitude = 50.215635, longitude = 1.711600, display_name = "Ponthoile"}, ["Noyelles-sur-Mer"] = { latitude = 50.182840, longitude = 1.707460, display_name = "Noyelles-sur-Mer"}, ["Cambron"] = { latitude = 50.111747, longitude = 1.769661, display_name = "Cambron"}, ["Morlancourt"] = { latitude = 49.950469, longitude = 2.628832, display_name = "Morlancourt"}, ["Etinehem-Mericourt"] = { latitude = 49.927788, longitude = 2.688331, display_name = "Etinehem-Mericourt"}, ["Franvillers"] = { latitude = 49.966473, longitude = 2.507860, display_name = "Franvillers"}, ["Ville-sur-Ancre"] = { latitude = 49.961707, longitude = 2.610144, display_name = "Ville-sur-Ancre"}, ["Moyenneville"] = { latitude = 50.070794, longitude = 1.749657, display_name = "Moyenneville"}, ["Yonval"] = { latitude = 50.089952, longitude = 1.789253, display_name = "Yonval"}, ["Franleu"] = { latitude = 50.098909, longitude = 1.639044, display_name = "Franleu"}, ["Acheux-en-Vimeu"] = { latitude = 50.064489, longitude = 1.675123, display_name = "Acheux-en-Vimeu"}, ["Beaucourt-sur-l'Hallue"] = { latitude = 49.986218, longitude = 2.444484, display_name = "Beaucourt-sur-l'Hallue"}, ["Varennes"] = { latitude = 50.049134, longitude = 2.534710, display_name = "Varennes"}, ["Senlis-le-Sec"] = { latitude = 50.024504, longitude = 2.578660, display_name = "Senlis-le-Sec"}, ["Bertrancourt"] = { latitude = 50.093358, longitude = 2.555917, display_name = "Bertrancourt"}, ["Bus-les-Artois"] = { latitude = 50.104208, longitude = 2.541676, display_name = "Bus-les-Artois"}, ["Authie"] = { latitude = 50.120757, longitude = 2.489984, display_name = "Authie"}, ["Marieux"] = { latitude = 50.106205, longitude = 2.441478, display_name = "Marieux"}, ["Louvencourt"] = { latitude = 50.094201, longitude = 2.499360, display_name = "Louvencourt"}, ["Vauchelles-les-Authie"] = { latitude = 50.095571, longitude = 2.473944, display_name = "Vauchelles-les-Authie"}, ["Raincheval"] = { latitude = 50.073792, longitude = 2.437531, display_name = "Raincheval"}, ["Arqueves"] = { latitude = 50.071686, longitude = 2.469114, display_name = "Arqueves"}, ["Acheux-en-Amienois"] = { latitude = 50.072899, longitude = 2.530620, display_name = "Acheux-en-Amienois"}, ["Puchevillers"] = { latitude = 50.054352, longitude = 2.409105, display_name = "Puchevillers"}, ["Beauquesne"] = { latitude = 50.084686, longitude = 2.394203, display_name = "Beauquesne"}, ["Terramesnil"] = { latitude = 50.106253, longitude = 2.379393, display_name = "Terramesnil"}, ["Humbercourt"] = { latitude = 50.210223, longitude = 2.454904, display_name = "Humbercourt"}, ["Brevillers"] = { latitude = 50.215587, longitude = 2.376572, display_name = "Brevillers"}, ["Grouches-Luchuel"] = { latitude = 50.179760, longitude = 2.381204, display_name = "Grouches-Luchuel"}, ["Neuvillette"] = { latitude = 50.209127, longitude = 2.319330, display_name = "Neuvillette"}, ["Bouquemaison"] = { latitude = 50.211581, longitude = 2.335495, display_name = "Bouquemaison"}, ["Le Souich"] = { latitude = 50.222057, longitude = 2.367014, display_name = "Le Souich"}, ["Allonville"] = { latitude = 49.939767, longitude = 2.364224, display_name = "Allonville"}, ["Meaulte"] = { latitude = 49.983122, longitude = 2.664155, display_name = "Meaulte"}, ["Saint-Sauveur"] = { latitude = 49.938093, longitude = 2.212011, display_name = "Saint-Sauveur"}, ["Dreuil-les-Amiens"] = { latitude = 49.914115, longitude = 2.233500, display_name = "Dreuil-les-Amiens"}, ["Quesnoy-sur-Airaines"] = { latitude = 49.956626, longitude = 1.990594, display_name = "Quesnoy-sur-Airaines"}, ["Saint-Vaast-en-Chaussee"] = { latitude = 49.969551, longitude = 2.204214, display_name = "Saint-Vaast-en-Chaussee"}, ["Vaux-en-Amienois"] = { latitude = 49.962606, longitude = 2.247945, display_name = "Vaux-en-Amienois"}, ["Poulainville"] = { latitude = 49.947348, longitude = 2.311602, display_name = "Poulainville"}, ["Coisy"] = { latitude = 49.959888, longitude = 2.326859, display_name = "Coisy"}, ["Cardonnette"] = { latitude = 49.952245, longitude = 2.359868, display_name = "Cardonnette"}, ["Rainneville"] = { latitude = 49.972235, longitude = 2.355317, display_name = "Rainneville"}, ["Contay"] = { latitude = 50.005329, longitude = 2.478641, display_name = "Contay"}, ["Warloy-Baillon"] = { latitude = 50.010790, longitude = 2.522951, display_name = "Warloy-Baillon"}, ["Baizieux"] = { latitude = 49.992474, longitude = 2.518960, display_name = "Baizieux"}, ["Henencourt"] = { latitude = 50.002171, longitude = 2.563231, display_name = "Henencourt"}, ["Millencourt"] = { latitude = 50.000761, longitude = 2.587326, display_name = "Millencourt"}, ["Mailly-Maillet"] = { latitude = 50.079767, longitude = 2.604249, display_name = "Mailly-Maillet"}, ["Vaire-sous-Corbie"] = { latitude = 49.914717, longitude = 2.546964, display_name = "Vaire-sous-Corbie"}, ["Heilly"] = { latitude = 49.953015, longitude = 2.536172, display_name = "Heilly"}, ["Sailly-le-Sec"] = { latitude = 49.920759, longitude = 2.582747, display_name = "Sailly-le-Sec"}, ["Sailly-Laurette"] = { latitude = 49.912221, longitude = 2.606709, display_name = "Sailly-Laurette"}, ["Oisemont"] = { latitude = 49.956082, longitude = 1.764936, display_name = "Oisemont"}, ["Allery"] = { latitude = 49.963050, longitude = 1.897986, display_name = "Allery"}, ["Colline-Beaumont"] = { latitude = 50.339831, longitude = 1.680815, display_name = "Colline-Beaumont"}, ["Fort-Mahon-Plage"] = { latitude = 50.341429, longitude = 1.568313, display_name = "Fort-Mahon-Plage"}, ["Talmas"] = { latitude = 50.029580, longitude = 2.325057, display_name = "Talmas"}, ["Neuilly-l'Hopital"] = { latitude = 50.170034, longitude = 1.878572, display_name = "Neuilly-l'Hopital"}, ["Riencourt"] = { latitude = 49.922113, longitude = 2.050381, display_name = "Riencourt"}, ["Cavillon"] = { latitude = 49.921717, longitude = 2.083588, display_name = "Cavillon"}, ["Belloy-sur-Somme"] = { latitude = 49.966846, longitude = 2.135439, display_name = "Belloy-sur-Somme"}, ["Warlus"] = { latitude = 50.275515, longitude = 2.668715, display_name = "Warlus"}, ["Willencourt"] = { latitude = 50.238886, longitude = 2.091478, display_name = "Willencourt"}, ["Acq"] = { latitude = 50.348457, longitude = 2.655962, display_name = "Acq"}, ["Agnieres"] = { latitude = 50.355059, longitude = 2.607809, display_name = "Agnieres"}, ["Aix-en-Issart"] = { latitude = 50.475171, longitude = 1.858967, display_name = "Aix-en-Issart"}, ["Ambricourt"] = { latitude = 50.467973, longitude = 2.175626, display_name = "Ambricourt"}, ["Ambrines"] = { latitude = 50.310247, longitude = 2.468694, display_name = "Ambrines"}, ["Aubigny-en-Artois"] = { latitude = 50.351595, longitude = 2.589628, display_name = "Aubigny-en-Artois"}, ["Aubin-Saint-Vaast"] = { latitude = 50.397660, longitude = 1.974479, display_name = "Aubin-Saint-Vaast"}, ["Aubrometz"] = { latitude = 50.304194, longitude = 2.177230, display_name = "Aubrometz"}, ["Bailleul-aux-Cornailles"] = { latitude = 50.371330, longitude = 2.443230, display_name = "Bailleul-aux-Cornailles"}, ["Bajus"] = { latitude = 50.421067, longitude = 2.481147, display_name = "Bajus"}, ["Le Titre"] = { latitude = 50.188785, longitude = 1.798457, display_name = "Le Titre"}, ["Beaumetz-les-Loges"] = { latitude = 50.243641, longitude = 2.655521, display_name = "Beaumetz-les-Loges"}, ["Berneville"] = { latitude = 50.265960, longitude = 2.670953, display_name = "Berneville"}, ["Bethonsart"] = { latitude = 50.375325, longitude = 2.551191, display_name = "Bethonsart"}, ["Beugin"] = { latitude = 50.442082, longitude = 2.514646, display_name = "Beugin"}, ["Bienvillers-au-Bois"] = { latitude = 50.174926, longitude = 2.620271, display_name = "Bienvillers-au-Bois"}, ["Boisjean"] = { latitude = 50.407192, longitude = 1.767314, display_name = "Boisjean"}, ["Bouin-Plumoison"] = { latitude = 50.381981, longitude = 1.988686, display_name = "Bouin-Plumoison"}, ["Buire-au-Bois"] = { latitude = 50.262317, longitude = 2.152020, display_name = "Buire-au-Bois"}, ["Buire-le-Sec"] = { latitude = 50.381962, longitude = 1.832624, display_name = "Buire-le-Sec"}, ["Crequy"] = { latitude = 50.494717, longitude = 2.046920, display_name = "Crequy"}, ["Carency"] = { latitude = 50.378876, longitude = 2.703589, display_name = "Carency"}, ["Campagne-les-Hesdin"] = { latitude = 50.398470, longitude = 1.878694, display_name = "Campagne-les-Hesdin"}, ["Camblain-Chatelain"] = { latitude = 50.482288, longitude = 2.463744, display_name = "Camblain-Chatelain"}, ["Camblain-l'Abbe"] = { latitude = 50.371375, longitude = 2.632892, display_name = "Camblain-l'Abbe"}, ["Canlers"] = { latitude = 50.482216, longitude = 2.142202, display_name = "Canlers"}, ["Capelle-les-Hesdin"] = { latitude = 50.341267, longitude = 1.998372, display_name = "Capelle-les-Hesdin"}, ["Caucourt"] = { latitude = 50.399633, longitude = 2.570931, display_name = "Caucourt"}, ["Chelers"] = { latitude = 50.375943, longitude = 2.484191, display_name = "Chelers"}, ["Coullemont"] = { latitude = 50.214546, longitude = 2.471392, display_name = "Coullemont"}, ["Couturelle"] = { latitude = 50.207486, longitude = 2.499051, display_name = "Couturelle"}, ["Crepy"] = { latitude = 50.472526, longitude = 2.201140, display_name = "Crepy"}, ["Drouvin-le-Marais"] = { latitude = 50.493415, longitude = 2.627614, display_name = "Drouvin-le-Marais"}, ["Embry"] = { latitude = 50.492782, longitude = 1.967237, display_name = "Embry"}, ["Estree-Cauchy"] = { latitude = 50.398375, longitude = 2.608935, display_name = "Estree-Cauchy"}, ["Etrun"] = { latitude = 50.314529, longitude = 2.700925, display_name = "Etrun"}, ["Wamin"] = { latitude = 50.413414, longitude = 2.058777, display_name = "Wamin"}, ["Fressin"] = { latitude = 50.446563, longitude = 2.055354, display_name = "Fressin"}, ["Ferfay"] = { latitude = 50.519583, longitude = 2.422769, display_name = "Ferfay"}, ["Foncquevillers"] = { latitude = 50.148739, longitude = 2.631933, display_name = "Foncquevillers"}, ["Fouquereuil"] = { latitude = 50.518441, longitude = 2.600400, display_name = "Fouquereuil"}, ["Fouquieres-les-Bethune"] = { latitude = 50.514124, longitude = 2.611136, display_name = "Fouquieres-les-Bethune"}, ["Frevillers"] = { latitude = 50.397655, longitude = 2.518940, display_name = "Frevillers"}, ["Frevin-Capelle"] = { latitude = 50.350238, longitude = 2.638829, display_name = "Frevin-Capelle"}, ["Gauchin-Legal"] = { latitude = 50.415413, longitude = 2.580783, display_name = "Gauchin-Legal"}, ["Gosnay"] = { latitude = 50.508288, longitude = 2.585141, display_name = "Gosnay"}, ["Gouy-Servins"] = { latitude = 50.402467, longitude = 2.649541, display_name = "Gouy-Servins"}, ["Guisy"] = { latitude = 50.389043, longitude = 2.001358, display_name = "Guisy"}, ["Hesmond"] = { latitude = 50.451619, longitude = 1.950778, display_name = "Hesmond"}, ["Habarcq"] = { latitude = 50.305638, longitude = 2.611375, display_name = "Habarcq"}, ["Hannescamps"] = { latitude = 50.166406, longitude = 2.638869, display_name = "Hannescamps"}, ["Haute-Avesnes"] = { latitude = 50.328849, longitude = 2.639675, display_name = "Haute-Avesnes"}, ["Hebuterne"] = { latitude = 50.125337, longitude = 2.636355, display_name = "Hebuterne"}, ["Hermaville"] = { latitude = 50.323366, longitude = 2.583539, display_name = "Hermaville"}, ["Hermin"] = { latitude = 50.418676, longitude = 2.558363, display_name = "Hermin"}, ["Hesdigneul-les-Bethune"] = { latitude = 50.501317, longitude = 2.593630, display_name = "Hesdigneul-les-Bethune"}, ["Houchin"] = { latitude = 50.482338, longitude = 2.619552, display_name = "Houchin"}, ["Izel-les-Hameau"] = { latitude = 50.314623, longitude = 2.531383, display_name = "Izel-les-Hameau"}, ["La Comte"] = { latitude = 50.427123, longitude = 2.499043, display_name = "La Comte"}, ["La Loge"] = { latitude = 50.410128, longitude = 2.031768, display_name = "La Loge"}, ["Labeuvriere"] = { latitude = 50.520376, longitude = 2.563110, display_name = "Labeuvriere"}, ["Lebiez"] = { latitude = 50.469253, longitude = 1.982758, display_name = "Lebiez"}, ["Lespinoy"] = { latitude = 50.426840, longitude = 1.878623, display_name = "Lespinoy"}, ["Loison-sur-Crequoise"] = { latitude = 50.437403, longitude = 1.925334, display_name = "Loison-sur-Crequoise"}, ["Lozinghem"] = { latitude = 50.517819, longitude = 2.499672, display_name = "Lozinghem"}, ["Maresquel-Ecquemicourt"] = { latitude = 50.408102, longitude = 1.933883, display_name = "Maresquel-Ecquemicourt"}, ["Magnicourt-en-Comte"] = { latitude = 50.403634, longitude = 2.486591, display_name = "Magnicourt-en-Comte"}, ["Magnicourt-sur-Canche"] = { latitude = 50.303018, longitude = 2.409031, display_name = "Magnicourt-sur-Canche"}, ["Maisnil-les-Ruitz"] = { latitude = 50.453376, longitude = 2.585746, display_name = "Maisnil-les-Ruitz"}, ["Maizieres"] = { latitude = 50.323792, longitude = 2.447266, display_name = "Maizieres"}, ["Manin"] = { latitude = 50.297024, longitude = 2.511387, display_name = "Manin"}, ["Mont-Saint-Eloi"] = { latitude = 50.351429, longitude = 2.692501, display_name = "Mont-Saint-Eloi"}, ["Marconnelle"] = { latitude = 50.373859, longitude = 2.018381, display_name = "Marconnelle"}, ["Marant"] = { latitude = 50.461734, longitude = 1.835891, display_name = "Marant"}, ["Mondicourt"] = { latitude = 50.173591, longitude = 2.462474, display_name = "Mondicourt"}, ["Marenla"] = { latitude = 50.446101, longitude = 1.868635, display_name = "Marenla"}, ["Mingoval"] = { latitude = 50.373024, longitude = 2.574754, display_name = "Mingoval"}, ["Puisieux"] = { latitude = 50.116703, longitude = 2.693897, display_name = "Puisieux"}, ["Ourton"] = { latitude = 50.455251, longitude = 2.477867, display_name = "Ourton"}, ["Pas-en-Artois"] = { latitude = 50.154193, longitude = 2.490704, display_name = "Pas-en-Artois"}, ["Penin"] = { latitude = 50.328137, longitude = 2.484400, display_name = "Penin"}, ["Pommera"] = { latitude = 50.172195, longitude = 2.442457, display_name = "Pommera"}, ["Pommier"] = { latitude = 50.184254, longitude = 2.599698, display_name = "Pommier"}, ["Royon"] = { latitude = 50.472399, longitude = 1.992301, display_name = "Royon"}, ["Villers-Chatel"] = { latitude = 50.376786, longitude = 2.585568, display_name = "Villers-Chatel"}, ["Noeux-les-Auxi"] = { latitude = 50.235132, longitude = 2.174807, display_name = "Noeux-les-Auxi"}, ["Rebreuve-Ranchicourt"] = { latitude = 50.437012, longitude = 2.558185, display_name = "Rebreuve-Ranchicourt"}, ["Rimboval"] = { latitude = 50.508085, longitude = 1.985654, display_name = "Rimboval"}, ["Ruisseauville"] = { latitude = 50.480788, longitude = 2.123324, display_name = "Ruisseauville"}, ["Ruitz"] = { latitude = 50.466503, longitude = 2.588501, display_name = "Ruitz"}, ["Sailly-Labourse"] = { latitude = 50.500806, longitude = 2.695613, display_name = "Sailly-Labourse"}, ["Verquigneul"] = { latitude = 50.502031, longitude = 2.665051, display_name = "Verquigneul"}, ["Vaudricourt"] = { latitude = 50.499351, longitude = 2.627253, display_name = "Vaudricourt"}, ["Avesnes-en-Val"] = { latitude = 49.920193, longitude = 1.398231, display_name = "Avesnes-en-Val"}, ["Sains-les-Fressin"] = { latitude = 50.466471, longitude = 2.040777, display_name = "Sains-les-Fressin"}, ["Saint-Amand"] = { latitude = 50.163989, longitude = 2.557766, display_name = "Saint-Amand"}, ["Saint-Denoeux"] = { latitude = 50.471621, longitude = 1.905215, display_name = "Saint-Denoeux"}, ["Savy-Berlette"] = { latitude = 50.353870, longitude = 2.570194, display_name = "Savy-Berlette"}, ["Sempy"] = { latitude = 50.492124, longitude = 1.876173, display_name = "Sempy"}, ["Servins"] = { latitude = 50.409664, longitude = 2.642820, display_name = "Servins"}, ["Sombrin"] = { latitude = 50.240107, longitude = 2.498782, display_name = "Sombrin"}, ["Saulty"] = { latitude = 50.216647, longitude = 2.534072, display_name = "Saulty"}, ["Souastre"] = { latitude = 50.152737, longitude = 2.564129, display_name = "Souastre"}, ["Tilloy-les-Hermaville"] = { latitude = 50.327542, longitude = 2.555282, display_name = "Tilloy-les-Hermaville"}, ["Tincques"] = { latitude = 50.358091, longitude = 2.493038, display_name = "Tincques"}, ["Villers-au-Bois"] = { latitude = 50.373476, longitude = 2.671491, display_name = "Villers-au-Bois"}, ["Villers-Brulin"] = { latitude = 50.368300, longitude = 2.539641, display_name = "Villers-Brulin"}, ["Barly"] = { latitude = 50.251285, longitude = 2.547855, display_name = "Barly"}, ["Beutin"] = { latitude = 50.491368, longitude = 1.723943, display_name = "Beutin"}, ["Bouret-sur-Canche"] = { latitude = 50.267561, longitude = 2.320685, display_name = "Bouret-sur-Canche"}, ["Fontaine-l'Etalon"] = { latitude = 50.304808, longitude = 2.062972, display_name = "Fontaine-l'Etalon"}, ["Fortel-en-Artois"] = { latitude = 50.258340, longitude = 2.224950, display_name = "Fortel-en-Artois"}, ["Humbercamps"] = { latitude = 50.184811, longitude = 2.574128, display_name = "Humbercamps"}, ["Liencourt"] = { latitude = 50.271114, longitude = 2.454495, display_name = "Liencourt"}, ["Quoeux-Haut-Mainil"] = { latitude = 50.299738, longitude = 2.109808, display_name = "Quoeux-Haut-Mainil"}, ["Friaucourt"] = { latitude = 50.088504, longitude = 1.477432, display_name = "Friaucourt"}, ["Allenay"] = { latitude = 50.090232, longitude = 1.494030, display_name = "Allenay"}, ["Brutelles"] = { latitude = 50.140851, longitude = 1.522464, display_name = "Brutelles"}, ["Vaudricourt"] = { latitude = 50.119989, longitude = 1.549379, display_name = "Vaudricourt"}, ["Saint-Blimont"] = { latitude = 50.120352, longitude = 1.567875, display_name = "Saint-Blimont"}, ["Nibas"] = { latitude = 50.099336, longitude = 1.588243, display_name = "Nibas"}, ["Lancheres"] = { latitude = 50.156881, longitude = 1.550766, display_name = "Lancheres"}, ["Pende"] = { latitude = 50.160419, longitude = 1.586523, display_name = "Pende"}, ["Saint-Quentin-Lamotte-Croix-au-Bailly"] = { latitude = 50.073532, longitude = 1.452558, display_name = "Saint-Quentin-Lamotte-Croix-au-Bailly"}, ["Mons-Boubert"] = { latitude = 50.128830, longitude = 1.662704, display_name = "Mons-Boubert"}, ["Saigneville"] = { latitude = 50.136906, longitude = 1.712312, display_name = "Saigneville"}, ["Ponts-et-Marais"] = { latitude = 50.040459, longitude = 1.442832, display_name = "Ponts-et-Marais"}, ["Woincourt"] = { latitude = 50.064974, longitude = 1.537203, display_name = "Woincourt"}, ["Dargnies"] = { latitude = 50.042273, longitude = 1.526040, display_name = "Dargnies"}, ["Beauchamps"] = { latitude = 50.017920, longitude = 1.508522, display_name = "Beauchamps"}, ["Embreville"] = { latitude = 50.029763, longitude = 1.542338, display_name = "Embreville"}, ["Mareuil-Caubert"] = { latitude = 50.068518, longitude = 1.829495, display_name = "Mareuil-Caubert"}, ["Huchenneville"] = { latitude = 50.050529, longitude = 1.798512, display_name = "Huchenneville"}, ["Aigneville"] = { latitude = 50.033267, longitude = 1.618383, display_name = "Aigneville"}, ["Bouillancourt-en-Sery"] = { latitude = 49.962078, longitude = 1.629067, display_name = "Bouillancourt-en-Sery"}, ["Port-le-Grand"] = { latitude = 50.151333, longitude = 1.749128, display_name = "Port-le-Grand"}, ["Favieres"] = { latitude = 50.237656, longitude = 1.663845, display_name = "Favieres"}, ["Villers-sur-Authie"] = { latitude = 50.317121, longitude = 1.690718, display_name = "Villers-sur-Authie"}, ["Forest-Montiers"] = { latitude = 50.245116, longitude = 1.741668, display_name = "Forest-Montiers"}, ["Buigny-Saint-Maclou"] = { latitude = 50.155218, longitude = 1.813248, display_name = "Buigny-Saint-Maclou"}, ["Vismes"] = { latitude = 50.011591, longitude = 1.672669, display_name = "Vismes"}, ["Toeufles"] = { latitude = 50.066957, longitude = 1.715689, display_name = "Toeufles"}, ["Behen"] = { latitude = 50.056974, longitude = 1.755319, display_name = "Behen"}, ["Bealencourt"] = { latitude = 50.435337, longitude = 2.121538, display_name = "Bealencourt"}, ["Fontaine-sur-Somme"] = { latitude = 50.029171, longitude = 1.941005, display_name = "Fontaine-sur-Somme"}, ["Longpre-les-Corps-Saints"] = { latitude = 50.013403, longitude = 1.991743, display_name = "Longpre-les-Corps-Saints"}, ["Bettencourt-Riviere"] = { latitude = 49.996197, longitude = 1.976653, display_name = "Bettencourt-Riviere"}, ["Conde-Folie"] = { latitude = 50.010437, longitude = 2.021068, display_name = "Conde-Folie"}, ["Hangest-sur-Somme"] = { latitude = 49.980232, longitude = 2.065232, display_name = "Hangest-sur-Somme"}, ["L'Etoile"] = { latitude = 50.020280, longitude = 2.045751, display_name = "L'Etoile"}, ["Huppy"] = { latitude = 50.024552, longitude = 1.764234, display_name = "Huppy"}, ["Bray-les-Mareuil"] = { latitude = 50.054739, longitude = 1.855303, display_name = "Bray-les-Mareuil"}, ["Erondelle"] = { latitude = 50.053317, longitude = 1.884265, display_name = "Erondelle"}, ["Limeux"] = { latitude = 50.019278, longitude = 1.815791, display_name = "Limeux"}, ["Frucourt"] = { latitude = 49.995513, longitude = 1.807079, display_name = "Frucourt"}, ["Crouy-Saint-Pierre"] = { latitude = 49.969409, longitude = 2.086307, display_name = "Crouy-Saint-Pierre"}, ["Frechencourt"] = { latitude = 49.965039, longitude = 2.441864, display_name = "Frechencourt"}, ["Calonne-Ricouart"] = { latitude = 50.489409, longitude = 2.484482, display_name = "Calonne-Ricouart"}, ["Stella-Plage"] = { latitude = 50.479940, longitude = 1.577138, display_name = "Stella-Plage"}, ["Recques-sur-Course"] = { latitude = 50.521554, longitude = 1.785264, display_name = "Recques-sur-Course"}, ["Fienvillers"] = { latitude = 50.117793, longitude = 2.228164, display_name = "Fienvillers"}, ["Houdain"] = { latitude = 50.453813, longitude = 2.536784, display_name = "Houdain"}, ["Pernois"] = { latitude = 50.051806, longitude = 2.181309, display_name = "Pernois"}, ["Caours"] = { latitude = 50.129588, longitude = 1.881779, display_name = "Caours"}, ["Saint-Riquier"] = { latitude = 50.135782, longitude = 1.946800, display_name = "Saint-Riquier"}, ["Neufmoulin"] = { latitude = 50.128545, longitude = 1.908033, display_name = "Neufmoulin"}, ["Millencourt-en-Ponthieu"] = { latitude = 50.151836, longitude = 1.901300, display_name = "Millencourt-en-Ponthieu"}, ["Hautvillers-Ouville"] = { latitude = 50.172747, longitude = 1.812496, display_name = "Hautvillers-Ouville"}, ["Forest-l'Abbaye"] = { latitude = 50.203667, longitude = 1.822246, display_name = "Forest-l'Abbaye"}, ["Marcheville"] = { latitude = 50.221449, longitude = 1.902782, display_name = "Marcheville"}, ["Bernay-en-Ponthieu"] = { latitude = 50.269179, longitude = 1.744496, display_name = "Bernay-en-Ponthieu"}, ["Arry"] = { latitude = 50.278391, longitude = 1.720847, display_name = "Arry"}, ["Regniere-Ecluse"] = { latitude = 50.279957, longitude = 1.769164, display_name = "Regniere-Ecluse"}, ["Vron"] = { latitude = 50.314832, longitude = 1.755027, display_name = "Vron"}, ["Nampont"] = { latitude = 50.347932, longitude = 1.745303, display_name = "Nampont"}, ["Vironchaux"] = { latitude = 50.285888, longitude = 1.828922, display_name = "Vironchaux"}, ["Vercourt"] = { latitude = 50.300467, longitude = 1.700853, display_name = "Vercourt"}, ["Machiel"] = { latitude = 50.269881, longitude = 1.822936, display_name = "Machiel"}, ["Machy"] = { latitude = 50.271234, longitude = 1.799760, display_name = "Machy"}, ["Sachin"] = { latitude = 50.486649, longitude = 2.375360, display_name = "Sachin"}, ["Boyaval"] = { latitude = 50.474299, longitude = 2.304088, display_name = "Boyaval"}, ["Tollent"] = { latitude = 50.277620, longitude = 2.013841, display_name = "Tollent"}, ["Siracourt"] = { latitude = 50.372126, longitude = 2.270515, display_name = "Siracourt"}, ["Wanquetin"] = { latitude = 50.276266, longitude = 2.612965, display_name = "Wanquetin"}, ["Simencourt"] = { latitude = 50.257176, longitude = 2.643434, display_name = "Simencourt"}, ["Wail"] = { latitude = 50.343675, longitude = 2.126523, display_name = "Wail"}, ["Villers-l'Hopital"] = { latitude = 50.228001, longitude = 2.213403, display_name = "Villers-l'Hopital"}, ["Beaucourt-sur-l'Ancre"] = { latitude = 50.079855, longitude = 2.687121, display_name = "Beaucourt-sur-l'Ancre"}, ["Basseux"] = { latitude = 50.225691, longitude = 2.644203, display_name = "Basseux"}, ["Bailleulval"] = { latitude = 50.221240, longitude = 2.634088, display_name = "Bailleulval"}, ["Riviere"] = { latitude = 50.233728, longitude = 2.685614, display_name = "Riviere"}, ["Ransart"] = { latitude = 50.209100, longitude = 2.687046, display_name = "Ransart"}, ["Bailleulmont"] = { latitude = 50.215951, longitude = 2.612239, display_name = "Bailleulmont"}, ["La Cauchie"] = { latitude = 50.201207, longitude = 2.582186, display_name = "La Cauchie"}, ["Monchy-au-Bois"] = { latitude = 50.179778, longitude = 2.657847, display_name = "Monchy-au-Bois"}, ["Gapennes"] = { latitude = 50.182745, longitude = 1.952581, display_name = "Gapennes"}, ["Buigny-l'Abbe"] = { latitude = 50.097853, longitude = 1.938007, display_name = "Buigny-l'Abbe"}, ["Bresle"] = { latitude = 49.983572, longitude = 2.557272, display_name = "Bresle"}, ["Fosseux"] = { latitude = 50.256212, longitude = 2.562805, display_name = "Fosseux"}, ["Grand-Rullecourt"] = { latitude = 50.255160, longitude = 2.473003, display_name = "Grand-Rullecourt"}, ["Bavincourt"] = { latitude = 50.225523, longitude = 2.567702, display_name = "Bavincourt"}, ["Gouy-en-Artois"] = { latitude = 50.247612, longitude = 2.592830, display_name = "Gouy-en-Artois"}, ["La Herliere"] = { latitude = 50.207756, longitude = 2.559321, display_name = "La Herliere"}, ["Neuville-sous-Montreuil"] = { latitude = 50.475693, longitude = 1.772682, display_name = "Neuville-sous-Montreuil"}, ["Ponches-Estruval"] = { latitude = 50.310024, longitude = 1.893326, display_name = "Ponches-Estruval"}, ["Le Ponchel"] = { latitude = 50.257722, longitude = 2.071385, display_name = "Le Ponchel"}, ["La Chaussee-Tirancourt"] = { latitude = 49.952940, longitude = 2.148340, display_name = "La Chaussee-Tirancourt"}, ["Ligescourt"] = { latitude = 50.288349, longitude = 1.876473, display_name = "Ligescourt"}, ["Canettemont"] = { latitude = 50.278814, longitude = 2.364928, display_name = "Canettemont"}, ["Montenescourt"] = { latitude = 50.293878, longitude = 2.622887, display_name = "Montenescourt"}, ["Saint-Georges"] = { latitude = 50.358395, longitude = 2.087431, display_name = "Saint-Georges"}, ["Berles-au-Bois"] = { latitude = 50.199002, longitude = 2.627526, display_name = "Berles-au-Bois"}, ["Bettencourt-Saint-Ouen"] = { latitude = 50.024972, longitude = 2.110299, display_name = "Bettencourt-Saint-Ouen"}, ["Lepine"] = { latitude = 50.385461, longitude = 1.735686, display_name = "Lepine"}, ["Floringhem"] = { latitude = 50.496938, longitude = 2.425733, display_name = "Floringhem"}, ["Buneville"] = { latitude = 50.324403, longitude = 2.357109, display_name = "Buneville"}, ["Laleu"] = { latitude = 49.941220, longitude = 1.932334, display_name = "Laleu"}, ["Agenvillers"] = { latitude = 50.176756, longitude = 1.917494, display_name = "Agenvillers"}, ["Boufflers"] = { latitude = 50.261571, longitude = 2.020652, display_name = "Boufflers"}, ["Brailly-Cornehotte"] = { latitude = 50.217280, longitude = 1.959455, display_name = "Brailly-Cornehotte"}, ["Domvast"] = { latitude = 50.198286, longitude = 1.921579, display_name = "Domvast"}, ["Estrees-les-Crecy"] = { latitude = 50.253385, longitude = 1.927624, display_name = "Estrees-les-Crecy"}, ["Fontaine-sur-Maye"] = { latitude = 50.235651, longitude = 1.925212, display_name = "Fontaine-sur-Maye"}, ["Gueschart"] = { latitude = 50.238987, longitude = 2.011477, display_name = "Gueschart"}, ["Lamotte-Buleux"] = { latitude = 50.188737, longitude = 1.827387, display_name = "Lamotte-Buleux"}, ["Noyelles-en-Chaussee"] = { latitude = 50.208602, longitude = 1.979836, display_name = "Noyelles-en-Chaussee"}, ["Agenville"] = { latitude = 50.165070, longitude = 2.102450, display_name = "Agenville"}, ["Autheux"] = { latitude = 50.142580, longitude = 2.228620, display_name = "Autheux"}, ["Barly"] = { latitude = 50.202120, longitude = 2.272490, display_name = "Barly"}, ["Bavelincourt"] = { latitude = 49.986440, longitude = 2.454680, display_name = "Bavelincourt"}, ["Beaumetz"] = { latitude = 50.140950, longitude = 2.119650, display_name = "Beaumetz"}, ["Bellancourt"] = { latitude = 50.090260, longitude = 1.909777, display_name = "Bellancourt"}, ["Bernaville"] = { latitude = 50.132351, longitude = 2.164393, display_name = "Bernaville"}, ["Bernatre"] = { latitude = 50.197320, longitude = 2.090690, display_name = "Bernatre"}, ["Bouchon"] = { latitude = 50.035275, longitude = 2.028775, display_name = "Bouchon"}, ["Bourdon"] = { latitude = 49.987030, longitude = 2.074940, display_name = "Bourdon"}, ["Brucamps"] = { latitude = 50.072220, longitude = 2.056640, display_name = "Brucamps"}, ["Bussus-Bussuel"] = { latitude = 50.109490, longitude = 1.998690, display_name = "Bussus-Bussuel"}, ["Canchy"] = { latitude = 50.185670, longitude = 1.876900, display_name = "Canchy"}, ["Conteville"] = { latitude = 50.177440, longitude = 2.074270, display_name = "Conteville"}, ["Coulonvillers"] = { latitude = 50.141880, longitude = 2.006440, display_name = "Coulonvillers"}, ["Cramont"] = { latitude = 50.148130, longitude = 2.053520, display_name = "Cramont"}, ["Domleger-Longvillers"] = { latitude = 50.159560, longitude = 2.085780, display_name = "Domleger-Longvillers"}, ["Domqueur"] = { latitude = 50.114380, longitude = 2.048880, display_name = "Domqueur"}, ["Ergnies"] = { latitude = 50.085540, longitude = 2.036850, display_name = "Ergnies"}, ["Francieres"] = { latitude = 50.072004, longitude = 1.940737, display_name = "Francieres"}, ["Franqueville"] = { latitude = 50.094150, longitude = 2.106500, display_name = "Franqueville"}, ["Fransu"] = { latitude = 50.109320, longitude = 2.092270, display_name = "Fransu"}, ["Frohen-sur-Authie"] = { latitude = 50.202322, longitude = 2.206451, display_name = "Frohen-sur-Authie"}, ["Gorenflos"] = { latitude = 50.095791, longitude = 2.049428, display_name = "Gorenflos"}, ["Heucourt-Croquoison"] = { latitude = 49.930668, longitude = 1.881624, display_name = "Heucourt-Croquoison"}, ["Heuzecourt"] = { latitude = 50.172710, longitude = 2.166640, display_name = "Heuzecourt"}, ["Hiermont"] = { latitude = 50.194510, longitude = 2.075350, display_name = "Hiermont"}, ["Le Meillard"] = { latitude = 50.170104, longitude = 2.194871, display_name = "Le Meillard"}, ["Le Mesge"] = { latitude = 49.944630, longitude = 2.052050, display_name = "Le Mesge"}, ["Maison-Ponthieu"] = { latitude = 50.206830, longitude = 2.042950, display_name = "Maison-Ponthieu"}, ["Maison-Roland"] = { latitude = 50.127893, longitude = 2.020570, display_name = "Maison-Roland"}, ["Maizicourt"] = { latitude = 50.195750, longitude = 2.121500, display_name = "Maizicourt"}, ["Mesnil-Domqueur"] = { latitude = 50.135190, longitude = 2.069450, display_name = "Mesnil-Domqueur"}, ["Montigny-les-Jongleurs"] = { latitude = 50.180560, longitude = 2.132950, display_name = "Montigny-les-Jongleurs"}, ["Mouflers"] = { latitude = 50.047010, longitude = 2.048740, display_name = "Mouflers"}, ["Metigny"] = { latitude = 49.944500, longitude = 1.926760, display_name = "Metigny"}, ["Mezerolles"] = { latitude = 50.186630, longitude = 2.234950, display_name = "Mezerolles"}, ["Neuilly-le-Dien"] = { latitude = 50.223000, longitude = 2.042640, display_name = "Neuilly-le-Dien"}, ["Occoches"] = { latitude = 50.174360, longitude = 2.269295, display_name = "Occoches"}, ["Oneux"] = { latitude = 50.145194, longitude = 1.970786, display_name = "Oneux"}, ["Outrebois"] = { latitude = 50.174160, longitude = 2.251920, display_name = "Outrebois"}, ["Prouville"] = { latitude = 50.147280, longitude = 2.125130, display_name = "Prouville"}, ["Ribeaucourt"] = { latitude = 50.116690, longitude = 2.118670, display_name = "Ribeaucourt"}, ["Saint-Acheul"] = { latitude = 50.190650, longitude = 2.163550, display_name = "Saint-Acheul"}, ["Saint-Gratien"] = { latitude = 49.965740, longitude = 2.408160, display_name = "Saint-Gratien"}, ["Soues"] = { latitude = 49.956320, longitude = 2.053220, display_name = "Soues"}, ["Surcamps"] = { latitude = 50.068760, longitude = 2.073450, display_name = "Surcamps"}, ["Vauchelles-les-Quesnoy"] = { latitude = 50.102607, longitude = 1.888834, display_name = "Vauchelles-les-Quesnoy"}, ["Villers-sous-Ailly"] = { latitude = 50.061130, longitude = 2.016390, display_name = "Villers-sous-Ailly"}, ["Vitz-sur-Authie"] = { latitude = 50.252906, longitude = 2.064327, display_name = "Vitz-sur-Authie"}, ["Yaucourt-Bussus"] = { latitude = 50.103020, longitude = 1.976480, display_name = "Yaucourt-Bussus"}, ["Yvrench"] = { latitude = 50.177790, longitude = 2.004340, display_name = "Yvrench"}, ["Yvrencheux"] = { latitude = 50.181246, longitude = 1.992521, display_name = "Yvrencheux"}, ["Yzeux"] = { latitude = 49.974700, longitude = 2.108600, display_name = "Yzeux"}, ["Longroy"] = { latitude = 49.988960, longitude = 1.535680, display_name = "Longroy"}, ["Rieux"] = { latitude = 49.934680, longitude = 1.580390, display_name = "Rieux"}, ["Agnez-les-Duisans"] = { latitude = 50.307060, longitude = 2.658735, display_name = "Agnez-les-Duisans"}, ["Alette"] = { latitude = 50.517430, longitude = 1.827260, display_name = "Alette"}, ["Attin"] = { latitude = 50.487875, longitude = 1.743784, display_name = "Attin"}, ["Auchy-les-Hesdin"] = { latitude = 50.398823, longitude = 2.101955, display_name = "Auchy-les-Hesdin"}, ["Aumerval"] = { latitude = 50.506482, longitude = 2.399188, display_name = "Aumerval"}, ["Averdoingt"] = { latitude = 50.343970, longitude = 2.441610, display_name = "Averdoingt"}, ["Bailleul-les-Pernes"] = { latitude = 50.510004, longitude = 2.387600, display_name = "Bailleul-les-Pernes"}, ["Beaufort-Blavincourt"] = { latitude = 50.278670, longitude = 2.496980, display_name = "Beaufort-Blavincourt"}, ["Beaumerie-Saint-Martin"] = { latitude = 50.455200, longitude = 1.797770, display_name = "Beaumerie-Saint-Martin"}, ["Beauvois"] = { latitude = 50.374280, longitude = 2.233510, display_name = "Beauvois"}, ["Bermicourt"] = { latitude = 50.408797, longitude = 2.230856, display_name = "Bermicourt"}, ["Blangerval-Blangermont"] = { latitude = 50.323080, longitude = 2.230080, display_name = "Blangerval-Blangermont"}, ["Blangy-sur-Ternoise"] = { latitude = 50.421450, longitude = 2.169560, display_name = "Blangy-sur-Ternoise"}, ["Blingel"] = { latitude = 50.407830, longitude = 2.147310, display_name = "Blingel"}, ["Bonnieres"] = { latitude = 50.244380, longitude = 2.259194, display_name = "Bonnieres"}, ["Boubers-sur-Canche"] = { latitude = 50.291293, longitude = 2.236901, display_name = "Boubers-sur-Canche"}, ["Bours"] = { latitude = 50.453854, longitude = 2.410986, display_name = "Bours"}, ["Campigneulles-les-Grandes"] = { latitude = 50.435490, longitude = 1.712490, display_name = "Campigneulles-les-Grandes"}, ["Cavron-Saint-Martin"] = { latitude = 50.417635, longitude = 1.999383, display_name = "Cavron-Saint-Martin"}, ["Cheriennes"] = { latitude = 50.313630, longitude = 2.035310, display_name = "Cheriennes"}, ["Clenleu"] = { latitude = 50.522310, longitude = 1.869390, display_name = "Clenleu"}, ["Contes"] = { latitude = 50.409136, longitude = 1.961080, display_name = "Contes"}, ["Conteville-en-Ternois"] = { latitude = 50.432745, longitude = 2.323704, display_name = "Conteville-en-Ternois"}, ["Croisette"] = { latitude = 50.352900, longitude = 2.260500, display_name = "Croisette"}, ["Croix-en-Ternois"] = { latitude = 50.383689, longitude = 2.280064, display_name = "Croix-en-Ternois"}, ["Denier"] = { latitude = 50.287307, longitude = 2.443385, display_name = "Denier"}, ["Duisans"] = { latitude = 50.305979, longitude = 2.688110, display_name = "Duisans"}, ["Eps"] = { latitude = 50.454550, longitude = 2.295560, display_name = "Eps"}, ["Estree-Wamin"] = { latitude = 50.269353, longitude = 2.394498, display_name = "Estree-Wamin"}, ["Estreelles"] = { latitude = 50.498150, longitude = 1.782670, display_name = "Estreelles"}, ["Flers"] = { latitude = 50.320530, longitude = 2.252550, display_name = "Flers"}, ["Fleury"] = { latitude = 50.421990, longitude = 2.253860, display_name = "Fleury"}, ["Fontaine-les-Hermans"] = { latitude = 50.526059, longitude = 2.349442, display_name = "Fontaine-les-Hermans"}, ["Foufflin-Ricametz"] = { latitude = 50.350106, longitude = 2.385308, display_name = "Foufflin-Ricametz"}, ["Framecourt"] = { latitude = 50.330239, longitude = 2.304849, display_name = "Framecourt"}, ["Fresnoy"] = { latitude = 50.367300, longitude = 2.128340, display_name = "Fresnoy"}, ["Galametz"] = { latitude = 50.327860, longitude = 2.137430, display_name = "Galametz"}, ["Gaudiempre"] = { latitude = 50.177793, longitude = 2.530880, display_name = "Gaudiempre"}, ["Givenchy-le-Noble"] = { latitude = 50.301509, longitude = 2.497085, display_name = "Givenchy-le-Noble"}, ["Gouves"] = { latitude = 50.298885, longitude = 2.635911, display_name = "Gouves"}, ["Gouy-en-Ternois"] = { latitude = 50.319846, longitude = 2.411787, display_name = "Gouy-en-Ternois"}, ["Grigny"] = { latitude = 50.385624, longitude = 2.065831, display_name = "Grigny"}, ["Guigny"] = { latitude = 50.329467, longitude = 1.998550, display_name = "Guigny"}, ["Guinecourt"] = { latitude = 50.348190, longitude = 2.225490, display_name = "Guinecourt"}, ["Herlin-le-Sec"] = { latitude = 50.354811, longitude = 2.330732, display_name = "Herlin-le-Sec"}, ["Herlincourt"] = { latitude = 50.344072, longitude = 2.302256, display_name = "Herlincourt"}, ["Hestrus"] = { latitude = 50.447560, longitude = 2.329400, display_name = "Hestrus"}, ["Houvin-Houvigneul"] = { latitude = 50.297612, longitude = 2.382772, display_name = "Houvin-Houvigneul"}, ["Huclier"] = { latitude = 50.429750, longitude = 2.356340, display_name = "Huclier"}, ["Humbert"] = { latitude = 50.503790, longitude = 1.906400, display_name = "Humbert"}, ["Humeroeuille"] = { latitude = 50.404779, longitude = 2.212303, display_name = "Humeroeuille"}, ["Humieres"] = { latitude = 50.387224, longitude = 2.203771, display_name = "Humieres"}, ["Hericourt"] = { latitude = 50.344780, longitude = 2.253600, display_name = "Hericourt"}, ["Incourt"] = { latitude = 50.390810, longitude = 2.151810, display_name = "Incourt"}, ["Ivergny"] = { latitude = 50.238690, longitude = 2.392370, display_name = "Ivergny"}, ["La Thieuloye"] = { latitude = 50.413245, longitude = 2.434301, display_name = "La Thieuloye"}, ["Lattre-Saint-Quentin"] = { latitude = 50.288187, longitude = 2.579949, display_name = "Lattre-Saint-Quentin"}, ["Le Quesnoy-en-Artois"] = { latitude = 50.333480, longitude = 2.047560, display_name = "Le Quesnoy-en-Artois"}, ["Lignereuil"] = { latitude = 50.290257, longitude = 2.472974, display_name = "Lignereuil"}, ["Ligny-Saint-Flochel"] = { latitude = 50.358524, longitude = 2.428236, display_name = "Ligny-Saint-Flochel"}, ["Ligny-sur-Canche"] = { latitude = 50.285305, longitude = 2.256618, display_name = "Ligny-sur-Canche"}, ["Linzeux"] = { latitude = 50.340901, longitude = 2.204534, display_name = "Linzeux"}, ["Maisnil"] = { latitude = 50.345400, longitude = 2.364430, display_name = "Maisnil"}, ["Maisoncelle"] = { latitude = 50.447300, longitude = 2.142420, display_name = "Maisoncelle"}, ["Marest"] = { latitude = 50.466914, longitude = 2.412207, display_name = "Marest"}, ["Marquay"] = { latitude = 50.382133, longitude = 2.421446, display_name = "Marquay"}, ["Moncheaux-les-Frevent"] = { latitude = 50.315119, longitude = 2.366697, display_name = "Moncheaux-les-Frevent"}, ["Monchel-sur-Canche"] = { latitude = 50.302352, longitude = 2.206383, display_name = "Monchel-sur-Canche"}, ["Monchiet"] = { latitude = 50.241219, longitude = 2.627880, display_name = "Monchiet"}, ["Monchy-Breton"] = { latitude = 50.400701, longitude = 2.440284, display_name = "Monchy-Breton"}, ["Monts-en-Ternois"] = { latitude = 50.321300, longitude = 2.384886, display_name = "Monts-en-Ternois"}, ["Neulette"] = { latitude = 50.381981, longitude = 2.166513, display_name = "Neulette"}, ["Neuville-au-Cornet"] = { latitude = 50.334717, longitude = 2.369580, display_name = "Neuville-au-Cornet"}, ["Noyelles-les-Humieres"] = { latitude = 50.373000, longitude = 2.174570, display_name = "Noyelles-les-Humieres"}, ["Noyellette"] = { latitude = 50.299034, longitude = 2.595646, display_name = "Noyellette"}, ["Nuncq-Hautecote"] = { latitude = 50.305812, longitude = 2.287741, display_name = "Nuncq-Hautecote"}, ["Nedon"] = { latitude = 50.524960, longitude = 2.369200, display_name = "Nedon"}, ["Nedonchel"] = { latitude = 50.524010, longitude = 2.357950, display_name = "Nedonchel"}, ["Orville"] = { latitude = 50.134376, longitude = 2.409865, display_name = "Orville"}, ["Ostreville"] = { latitude = 50.395774, longitude = 2.394440, display_name = "Ostreville"}, ["Pierremont"] = { latitude = 50.401179, longitude = 2.259532, display_name = "Pierremont"}, ["Predefin"] = { latitude = 50.502780, longitude = 2.254070, display_name = "Predefin"}, ["Quilen"] = { latitude = 50.529100, longitude = 1.926980, display_name = "Quilen"}, ["Raye-sur-Authie"] = { latitude = 50.297650, longitude = 1.947310, display_name = "Raye-sur-Authie"}, ["Rebreuve-sur-Canche"] = { latitude = 50.264927, longitude = 2.340054, display_name = "Rebreuve-sur-Canche"}, ["Rebreuviette"] = { latitude = 50.262936, longitude = 2.361126, display_name = "Rebreuviette"}, ["Rollancourt"] = { latitude = 50.407650, longitude = 2.122020, display_name = "Rollancourt"}, ["Roellecourt"] = { latitude = 50.366485, longitude = 2.389433, display_name = "Roellecourt"}, ["Sains-les-Pernes"] = { latitude = 50.477320, longitude = 2.355730, display_name = "Sains-les-Pernes"}, ["Saint-Michel-sous-Bois"] = { latitude = 50.513880, longitude = 1.931520, display_name = "Saint-Michel-sous-Bois"}, ["Saint-Michel-sur-Ternoise"] = { latitude = 50.376360, longitude = 2.357629, display_name = "Saint-Michel-sur-Ternoise"}, ["Sars-le-Bois"] = { latitude = 50.294530, longitude = 2.428140, display_name = "Sars-le-Bois"}, ["Sibiville"] = { latitude = 50.298490, longitude = 2.322970, display_name = "Sibiville"}, ["Sus-Saint-Leger"] = { latitude = 50.238951, longitude = 2.432944, display_name = "Sus-Saint-Leger"}, ["Sericourt"] = { latitude = 50.294063, longitude = 2.314864, display_name = "Sericourt"}, ["Teneur"] = { latitude = 50.450490, longitude = 2.218290, display_name = "Teneur"}, ["Ternas"] = { latitude = 50.341970, longitude = 2.396740, display_name = "Ternas"}, ["Tilly-Capelle"] = { latitude = 50.443180, longitude = 2.197320, display_name = "Tilly-Capelle"}, ["Tramecourt"] = { latitude = 50.463510, longitude = 2.150580, display_name = "Tramecourt"}, ["Troisvaux"] = { latitude = 50.401834, longitude = 2.343038, display_name = "Troisvaux"}, ["Vaulx"] = { latitude = 50.267362, longitude = 2.096494, display_name = "Vaulx"}, ["Wambercourt"] = { latitude = 50.428460, longitude = 2.023280, display_name = "Wambercourt"}, ["Warluzel"] = { latitude = 50.228915, longitude = 2.470100, display_name = "Warluzel"}, ["Willeman"] = { latitude = 50.353056, longitude = 2.156880, display_name = "Willeman"}, ["Eclimeux"] = { latitude = 50.398789, longitude = 2.178689, display_name = "Eclimeux"}, ["Ecoivres"] = { latitude = 50.322530, longitude = 2.287590, display_name = "Ecoivres"}, ["Ecuires"] = { latitude = 50.444689, longitude = 1.763966, display_name = "Ecuires"}, ["Equirre"] = { latitude = 50.472375, longitude = 2.236755, display_name = "Equirre"}, ["Erin"] = { latitude = 50.438760, longitude = 2.208630, display_name = "Erin"}, ["Oeuf-en-Ternois"] = { latitude = 50.358938, longitude = 2.211600, display_name = "Oeuf-en-Ternois"}, ["Groffliers"] = { latitude = 50.382729, longitude = 1.622814, display_name = "Groffliers"}, ["Tubersent"] = { latitude = 50.519779, longitude = 1.704451, display_name = "Tubersent"}, ["Maresville"] = { latitude = 50.526554, longitude = 1.731015, display_name = "Maresville"}, ["Brexent-Enocq"] = { latitude = 50.509592, longitude = 1.728742, display_name = "Brexent-Enocq"}, ["La Madelaine-sous-Montreuil"] = { latitude = 50.468627, longitude = 1.749413, display_name = "La Madelaine-sous-Montreuil"}, ["Airon-Saint-Vaast"] = { latitude = 50.431377, longitude = 1.669146, display_name = "Airon-Saint-Vaast"}, ["Airon-Notre-Dame"] = { latitude = 50.436507, longitude = 1.655822, display_name = "Airon-Notre-Dame"}, ["Verton"] = { latitude = 50.401553, longitude = 1.650798, display_name = "Verton"}, ["Caumont"] = { latitude = 50.289437, longitude = 2.029945, display_name = "Caumont"}, ["Fillievres"] = { latitude = 50.319387, longitude = 2.158198, display_name = "Fillievres"}, ["Conchil-le-Temple"] = { latitude = 50.370928, longitude = 1.665754, display_name = "Conchil-le-Temple"}, ["Tigny-Noyelle"] = { latitude = 50.351400, longitude = 1.717952, display_name = "Tigny-Noyelle"}, ["Waben"] = { latitude = 50.380072, longitude = 1.653436, display_name = "Waben"}, ["Coupelle-Vieille"] = { latitude = 50.524957, longitude = 2.099054, display_name = "Coupelle-Vieille"}, ["Saint-Remy-au-Bois"] = { latitude = 50.368111, longitude = 1.873131, display_name = "Saint-Remy-au-Bois"}, ["Avondance"] = { latitude = 50.476049, longitude = 2.098952, display_name = "Avondance"}, ["Boubers-les-Hesmond"] = { latitude = 50.475020, longitude = 1.949731, display_name = "Boubers-les-Hesmond"}, ["Coupelle-Neuve"] = { latitude = 50.500689, longitude = 2.119716, display_name = "Coupelle-Neuve"}, ["Offin"] = { latitude = 50.445176, longitude = 1.942315, display_name = "Offin"}, ["Torcy"] = { latitude = 50.483063, longitude = 2.023020, display_name = "Torcy"}, ["Sainte-Austreberthe"] = { latitude = 50.363513, longitude = 2.046351, display_name = "Sainte-Austreberthe"}, ["Beauvoir-Wavans"] = { latitude = 50.218474, longitude = 2.163085, display_name = "Beauvoir-Wavans"}, ["Conchy-sur-Canche"] = { latitude = 50.300792, longitude = 2.195983, display_name = "Conchy-sur-Canche"}, ["Gennes-Ivergny"] = { latitude = 50.264791, longitude = 2.047602, display_name = "Gennes-Ivergny"}, ["Rougefay"] = { latitude = 50.272935, longitude = 2.171032, display_name = "Rougefay"}, ["Vacquerie-le-Boucq"] = { latitude = 50.269647, longitude = 2.218496, display_name = "Vacquerie-le-Boucq"}, ["Vieil-Hesdin"] = { latitude = 50.356193, longitude = 2.098040, display_name = "Vieil-Hesdin"}, ["Boffles"] = { latitude = 50.253988, longitude = 2.202852, display_name = "Boffles"}, ["Canteleux"] = { latitude = 50.216291, longitude = 2.306973, display_name = "Canteleux"}, ["Pressy"] = { latitude = 50.475604, longitude = 2.397533, display_name = "Pressy"}, ["Dieval"] = { latitude = 50.434914, longitude = 2.448034, display_name = "Dieval"}, ["Berles-Monchel"] = { latitude = 50.345477, longitude = 2.537601, display_name = "Berles-Monchel"}, ["Berlencourt-le-Cauroy"] = { latitude = 50.279325, longitude = 2.423454, display_name = "Berlencourt-le-Cauroy"}, ["Noyelle-Vion"] = { latitude = 50.294571, longitude = 2.547255, display_name = "Noyelle-Vion"}, ["Villers-Sir-Simon"] = { latitude = 50.317131, longitude = 2.491699, display_name = "Villers-Sir-Simon"}, ["Warlincourt-les-Pas"] = { latitude = 50.174255, longitude = 2.505313, display_name = "Warlincourt-les-Pas"}, ["Sailly-au-Bois"] = { latitude = 50.119868, longitude = 2.595678, display_name = "Sailly-au-Bois"}, ["Hem-Hardinval"] = { latitude = 50.163187, longitude = 2.303552, display_name = "Hem-Hardinval"}, ["Vauchelles-les-Domart"] = { latitude = 50.055224, longitude = 2.057334, display_name = "Vauchelles-les-Domart"}, ["Herissart"] = { latitude = 50.027715, longitude = 2.415546, display_name = "Herissart"}, ["Dompierre-sur-Authie"] = { latitude = 50.303624, longitude = 1.916875, display_name = "Dompierre-sur-Authie"}, ["Incheville"] = { latitude = 50.015656, longitude = 1.497900, display_name = "Incheville"}, ["Saint-Pierre-en-Val"] = { latitude = 50.021065, longitude = 1.446089, display_name = "Saint-Pierre-en-Val"}, ["Fourdrinoy"] = { latitude = 49.917409, longitude = 2.107841, display_name = "Fourdrinoy"}, ["Hauteville"] = { latitude = 50.273637, longitude = 2.573024, display_name = "Hauteville"}, ["Acquet"] = { latitude = 50.226910, longitude = 2.056536, display_name = "Acquet"}, ["Labroye"] = { latitude = 50.278221, longitude = 1.989908, display_name = "Labroye"}, ["Vignacourt"] = { latitude = 50.011839, longitude = 2.196777, display_name = "Vignacourt"}, ["Lealvillers"] = { latitude = 50.065428, longitude = 2.509457, display_name = "Lealvillers"}, ["Fieffes-Montrelet"] = { latitude = 50.083938, longitude = 2.231165, display_name = "Fieffes-Montrelet"}, ["Beaurainville"] = { latitude = 50.424843, longitude = 1.900946, display_name = "Beaurainville"}, ["Heudelimont"] = { latitude = 50.011780, longitude = 1.375816, display_name = "Heudelimont"}, ["Valencendre"] = { latitude = 50.485801, longitude = 1.693873, display_name = "Valencendre"}, ["Boisbergues"] = { latitude = 50.156183, longitude = 2.229787, display_name = "Boisbergues"}, ["Auchonvillers"] = { latitude = 50.081345, longitude = 2.630008, display_name = "Auchonvillers"}, ["Rue"] = { latitude = 50.272874, longitude = 1.667305, display_name = "Rue"}, ["Amiens"] = { latitude = 49.899546, longitude = 2.295089, display_name = "Amiens"}, ["Corbie"] = { latitude = 49.908415, longitude = 2.510957, display_name = "Corbie"}, ["Arras"] = { latitude = 50.288691, longitude = 2.776158, display_name = "Arras"}, ["Douai"] = { latitude = 50.367849, longitude = 3.089882, display_name = "Douai"}, ["Lens"] = { latitude = 50.427170, longitude = 2.832500, display_name = "Lens"}, } veafNamedPoints._citiesSyria = { ["Racetrack for Camel Racing"] = { latitude = 34.403706, longitude = 38.198771, display_name = ("Racetrack for Camel Racing")}, ["Marbat El-Hassan Reservoir"] = { latitude = 34.667083, longitude = 38.224691, display_name = ("Marbat El-Hassan Reservoir")}, ["Sharqiyah Mine"] = { latitude = 34.199641, longitude = 38.014742, display_name = ("Sharqiyah Mine")}, ["Solonchak Sabhat al-Jabbul"] = { latitude = 36.044571, longitude = 37.521323, display_name = ("Solonchak Sabhat al-Jabbul")}, ["Adana"] = { latitude = 37.009025, longitude = 35.305756, display_name = ("Adana")}, ["Aleppo"] = { latitude = 36.206786, longitude = 37.142391, display_name = ("Aleppo")}, ["Raqqa"] = { latitude = 35.960290, longitude = 39.015335, display_name = ("Raqqa")}, ["Latakia"] = { latitude = 35.525744, longitude = 35.785411, display_name = ("Latakia")}, ["Hama"] = { latitude = 35.147288, longitude = 36.757011, display_name = ("Hama")}, ["Homs"] = { latitude = 34.731897, longitude = 36.711724, display_name = ("Homs")}, ["Palmyra"] = { latitude = 34.565481, longitude = 38.284229, display_name = ("Palmyra")}, ["Tartus"] = { latitude = 34.893779, longitude = 35.892245, display_name = ("Tartus")}, ["Tripoli"] = { latitude = 34.435775, longitude = 35.837320, display_name = ("Tripoli")}, ["Beirut"] = { latitude = 33.861187, longitude = 35.526848, display_name = ("Beirut")}, ["Damascus"] = { latitude = 33.518073, longitude = 36.296386, display_name = ("Damascus")}, ["Haifa"] = { latitude = 32.813432, longitude = 34.987236, display_name = ("Haifa")}, ["Idlib"] = { latitude = 35.930952, longitude = 36.633923, display_name = ("Idlib")}, ["Qudssaya"] = { latitude = 33.536740, longitude = 36.234351, display_name = ("Qudssaya")}, ["Qudssaya Suburb"] = { latitude = 33.540064, longitude = 36.192270, display_name = ("Qudssaya Suburb")}, ["At Tall"] = { latitude = 33.600323, longitude = 36.315657, display_name = ("At Tall")}, ["Alassad"] = { latitude = 33.581558, longitude = 36.358940, display_name = ("Alassad")}, ["Duma"] = { latitude = 33.571938, longitude = 36.405781, display_name = ("Duma")}, ["Harasta"] = { latitude = 33.559036, longitude = 36.366581, display_name = ("Harasta")}, ["Arbil"] = { latitude = 33.539070, longitude = 36.366804, display_name = ("Arbil")}, ["Madryara"] = { latitude = 33.544681, longitude = 36.395586, display_name = ("Madryara")}, ["Ad Dumayr"] = { latitude = 33.643665, longitude = 36.691055, display_name = ("Ad Dumayr")}, ["Al Qutayfah"] = { latitude = 33.741700, longitude = 36.594686, display_name = ("Al Qutayfah")}, ["Kafr Batna"] = { latitude = 33.514919, longitude = 36.384188, display_name = ("Kafr Batna")}, ["Ein Tamra"] = { latitude = 33.518276, longitude = 36.352033, display_name = ("Ein Tamra")}, ["Al Mleha"] = { latitude = 33.484569, longitude = 36.373028, display_name = ("Al Mleha")}, ["Babbila"] = { latitude = 33.477200, longitude = 36.336357, display_name = ("Babbila")}, ["El Hajar Al Aswad"] = { latitude = 33.468749, longitude = 36.309625, display_name = ("El Hajar Al Aswad")}, ["Set Zaynab"] = { latitude = 33.448837, longitude = 36.338214, display_name = ("Set Zaynab")}, ["Shebaa"] = { latitude = 33.449565, longitude = 36.398202, display_name = ("Shebaa")}, ["Al Ghuzlaniyah"] = { latitude = 33.398953, longitude = 36.454461, display_name = ("Al Ghuzlaniyah")}, ["Jdaydet Alkhas"] = { latitude = 33.405680, longitude = 36.544628, display_name = ("Jdaydet Alkhas")}, ["Harran al'Awamid"] = { latitude = 33.447575, longitude = 36.561596, display_name = ("Harran al'Awamid")}, ["Otaybah"] = { latitude = 33.483517, longitude = 36.610154, display_name = ("Otaybah")}, ["Hayjanah"] = { latitude = 33.358906, longitude = 36.544664, display_name = ("Hayjanah")}, ["Al Baytariyah"] = { latitude = 33.315697, longitude = 36.543629, display_name = ("Al Baytariyah")}, ["Buraq"] = { latitude = 33.185661, longitude = 36.479501, display_name = ("Buraq")}, ["Hazm"] = { latitude = 33.132337, longitude = 36.524319, display_name = ("Hazm")}, ["Qura Al-Assad"] = { latitude = 33.556956, longitude = 36.135584, display_name = ("Qura Al-Assad")}, ["Al-Dimass"] = { latitude = 33.588011, longitude = 36.091209, display_name = ("Al-Dimass")}, ["Al Moadamyeh"] = { latitude = 33.463478, longitude = 36.187321, display_name = ("Al Moadamyeh")}, ["Darayya"] = { latitude = 33.460294, longitude = 36.235558, display_name = ("Darayya")}, ["Sahnaya"] = { latitude = 33.432227, longitude = 36.237736, display_name = ("Sahnaya")}, ["Jdaydet Artooz"] = { latitude = 33.438519, longitude = 36.159494, display_name = ("Jdaydet Artooz")}, ["Zakyah"] = { latitude = 33.337249, longitude = 36.165773, display_name = ("Zakyah")}, ["As Sanamayn"] = { latitude = 33.077101, longitude = 36.185811, display_name = ("As Sanamayn")}, ["Jasim"] = { latitude = 32.996681, longitude = 36.064041, display_name = ("Jasim")}, ["Ankhul"] = { latitude = 33.017328, longitude = 36.130427, display_name = ("Ankhul")}, ["Kafr Shams"] = { latitude = 33.122908, longitude = 36.115409, display_name = ("Kafr Shams")}, ["Aqrabac"] = { latitude = 33.111487, longitude = 36.001655, display_name = ("Aqrabac")}, ["Al Harah"] = { latitude = 33.057838, longitude = 36.006797, display_name = ("Al Harah")}, ["Cotlu"] = { latitude = 36.874264, longitude = 35.487177, display_name = ("Cotlu")}, ["Ceyhan"] = { latitude = 37.032204, longitude = 35.820925, display_name = ("Ceyhan")}, ["Karsi"] = { latitude = 36.761068, longitude = 36.223837, display_name = ("Karsi")}, ["Sariseki"] = { latitude = 36.629934, longitude = 36.220243, display_name = ("Sariseki")}, ["Iskenderun"] = { latitude = 36.570207, longitude = 36.147166, display_name = ("Iskenderun")}, ["Kirikhan"] = { latitude = 36.501251, longitude = 36.361665, display_name = ("Kirikhan")}, ["Akincilar"] = { latitude = 36.388407, longitude = 36.231198, display_name = ("Akincilar")}, ["Kumlu"] = { latitude = 36.365700, longitude = 36.465543, display_name = ("Kumlu")}, ["Reyhanli"] = { latitude = 36.263975, longitude = 36.567831, display_name = ("Reyhanli")}, ["Antakya"] = { latitude = 36.211221, longitude = 36.157590, display_name = ("Antakya")}, ["Samandag"] = { latitude = 36.081880, longitude = 35.981085, display_name = ("Samandag")}, ["Maarrat Misrin"] = { latitude = 36.013735, longitude = 36.678771, display_name = ("Maarrat Misrin")}, ["Sarmin"] = { latitude = 35.906478, longitude = 36.725726, display_name = ("Sarmin")}, ["Saraqib"] = { latitude = 35.861085, longitude = 36.808029, display_name = ("Saraqib")}, ["Arihah"] = { latitude = 35.819453, longitude = 36.616768, display_name = ("Arihah")}, ["Al Rami"] = { latitude = 35.804366, longitude = 36.487599, display_name = ("Al Rami")}, ["Jisr Ash-Shughur"] = { latitude = 35.814383, longitude = 36.318373, display_name = ("Jisr Ash-Shughur")}, ["Khan Assubul"] = { latitude = 35.751556, longitude = 36.758686, display_name = ("Khan Assubul")}, ["Abu Ad Dhuhur"] = { latitude = 35.749513, longitude = 37.049436, display_name = ("Abu Ad Dhuhur")}, ["Al Ghadfah"] = { latitude = 35.677496, longitude = 36.801608, display_name = ("Al Ghadfah")}, ["Maarat al-Numan"] = { latitude = 35.641889, longitude = 36.675356, display_name = ("Maarat al-Numan")}, ["Kafr Nabi"] = { latitude = 35.614837, longitude = 36.568447, display_name = ("Kafr Nabi")}, ["Tell Arn"] = { latitude = 36.124066, longitude = 37.333972, display_name = ("Tell Arn")}, ["Al-Safirah"] = { latitude = 36.069189, longitude = 37.377062, display_name = ("Al-Safirah")}, ["Al Bab"] = { latitude = 36.366231, longitude = 37.512118, display_name = ("Al Bab")}, ["Manbij"] = { latitude = 36.525941, longitude = 37.960087, display_name = ("Manbij")}, ["Maskanah"] = { latitude = 35.964663, longitude = 38.042601, display_name = ("Maskanah")}, ["Al Tabqah"] = { latitude = 35.827092, longitude = 38.541302, display_name = ("Al Tabqah")}, ["Sqoubin"] = { latitude = 35.558660, longitude = 35.829224, display_name = ("Sqoubin")}, ["Sett Markho"] = { latitude = 35.589482, longitude = 35.851695, display_name = ("Sett Markho")}, ["Al Hannadi"] = { latitude = 35.480731, longitude = 35.927036, display_name = ("Al Hannadi")}, ["Jablah"] = { latitude = 35.363965, longitude = 35.927291, display_name = ("Jablah")}, ["Baniyas"] = { latitude = 35.189238, longitude = 35.953516, display_name = ("Baniyas")}, ["Ein Elkorum"] = { latitude = 35.368802, longitude = 36.409242, display_name = ("Ein Elkorum")}, ["Muhradah"] = { latitude = 35.249643, longitude = 36.572717, display_name = ("Muhradah")}, ["Halfaya"] = { latitude = 35.260712, longitude = 36.606798, display_name = ("Halfaya")}, ["Ar Rastan"] = { latitude = 34.923488, longitude = 36.732503, display_name = ("Ar Rastan")}, ["Hawash"] = { latitude = 34.762026, longitude = 36.322719, display_name = ("Hawash")}, ["Zaidal"] = { latitude = 34.718672, longitude = 36.773686, display_name = ("Zaidal")}, ["Fairuzah"] = { latitude = 34.702436, longitude = 36.757134, display_name = ("Fairuzah")}, ["Al Qusayr"] = { latitude = 34.512113, longitude = 36.587781, display_name = ("Al Qusayr")}, ["Khirbe"] = { latitude = 34.583839, longitude = 36.017331, display_name = ("Khirbe")}, ["Al Aabde"] = { latitude = 34.511339, longitude = 35.962268, display_name = ("Al Aabde")}, ["Chekka"] = { latitude = 34.323663, longitude = 35.731135, display_name = ("Chekka")}, ["Hamat"] = { latitude = 34.285622, longitude = 35.692717, display_name = ("Hamat")}, ["Batroun"] = { latitude = 34.252986, longitude = 35.666329, display_name = ("Batroun")}, ["Maqne"] = { latitude = 34.076334, longitude = 36.206335, display_name = ("Maqne")}, ["Baalbek"] = { latitude = 34.005549, longitude = 36.204317, display_name = ("Baalbek")}, ["Jounieh"] = { latitude = 33.979290, longitude = 35.633664, display_name = ("Jounieh")}, ["Rayak"] = { latitude = 33.853182, longitude = 36.021607, display_name = ("Rayak")}, ["Serghaya"] = { latitude = 33.811850, longitude = 36.161077, display_name = ("Serghaya")}, ["Al Zabadani"] = { latitude = 33.726458, longitude = 36.101596, display_name = ("Al Zabadani")}, ["Madaya"] = { latitude = 33.682698, longitude = 36.095064, display_name = ("Madaya")}, ["Zahle"] = { latitude = 33.842402, longitude = 35.925711, display_name = ("Zahle")}, ["Taalabaya"] = { latitude = 33.814138, longitude = 35.873182, display_name = ("Taalabaya")}, ["Bar Elias"] = { latitude = 33.774479, longitude = 35.900918, display_name = ("Bar Elias")}, ["Anjar"] = { latitude = 33.727388, longitude = 35.931892, display_name = ("Anjar")}, ["Majdel Anjar"] = { latitude = 33.707532, longitude = 35.907553, display_name = ("Majdel Anjar")}, ["Ghazze"] = { latitude = 33.668655, longitude = 35.831698, display_name = ("Ghazze")}, ["Joub Jannine"] = { latitude = 33.627189, longitude = 35.783124, display_name = ("Joub Jannine")}, ["Qaraoun"] = { latitude = 33.567601, longitude = 35.721222, display_name = ("Qaraoun")}, ["Khalde"] = { latitude = 33.777551, longitude = 35.475219, display_name = ("Khalde")}, ["Haret Chbib"] = { latitude = 33.740920, longitude = 35.457533, display_name = ("Haret Chbib")}, ["Chim"] = { latitude = 33.620974, longitude = 35.488638, display_name = ("Chim")}, ["Saida"] = { latitude = 33.563094, longitude = 35.377028, display_name = ("Saida")}, ["Nabatieh"] = { latitude = 33.381200, longitude = 35.479974, display_name = ("Nabatieh")}, ["Qatana"] = { latitude = 33.438131, longitude = 36.079687, display_name = ("Qatana")}, ["Khan Alsheh"] = { latitude = 33.373053, longitude = 36.113067, display_name = ("Khan Alsheh")}, ["Kanaker"] = { latitude = 33.268693, longitude = 36.094640, display_name = ("Kanaker")}, ["Jabah"] = { latitude = 33.164877, longitude = 35.927233, display_name = ("Jabah")}, ["Khan Arnabeh"] = { latitude = 33.182506, longitude = 35.890276, display_name = ("Khan Arnabeh")}, ["Naba Alsakher"] = { latitude = 33.088327, longitude = 35.947298, display_name = ("Naba Alsakher")}, ["Ghabagheb"] = { latitude = 33.182552, longitude = 36.224414, display_name = ("Ghabagheb")}, ["Jabab"] = { latitude = 33.113405, longitude = 36.264712, display_name = ("Jabab")}, ["As Sawara"] = { latitude = 33.028305, longitude = 36.578694, display_name = ("As Sawara")}, ["Shahba"] = { latitude = 32.858367, longitude = 36.632505, display_name = ("Shahba")}, ["Muadamyat Al Qalamon"] = { latitude = 33.741316, longitude = 36.640933, display_name = ("Muadamyat Al Qalamon")}, ["Al Kafr"] = { latitude = 32.632244, longitude = 36.648252, display_name = ("Al Kafr")}, ["El Karak"] = { latitude = 32.685853, longitude = 36.353277, display_name = ("El Karak")}, ["Eastern Garyiah"] = { latitude = 32.677965, longitude = 36.260940, display_name = ("Eastern Garyiah")}, ["Western Garyiah"] = { latitude = 32.687917, longitude = 36.225685, display_name = ("Western Garyiah")}, ["Khirbet Ghazaleh"] = { latitude = 32.737882, longitude = 36.201831, display_name = ("Khirbet Ghazaleh")}, ["Busra"] = { latitude = 32.517997, longitude = 36.481645, display_name = ("Busra")}, ["Dibin"] = { latitude = 32.439463, longitude = 36.567784, display_name = ("Dibin")}, ["Miarbah"] = { latitude = 32.544797, longitude = 36.428341, display_name = ("Miarbah")}, ["Ghasam"] = { latitude = 32.549096, longitude = 36.374564, display_name = ("Ghasam")}, ["Al Jeezah"] = { latitude = 32.565680, longitude = 36.316322, display_name = ("Al Jeezah")}, ["El Taebah"] = { latitude = 32.564137, longitude = 36.246336, display_name = ("El Taebah")}, ["Ramtha"] = { latitude = 32.565921, longitude = 36.008365, display_name = ("Ramtha")}, ["Et Turra"] = { latitude = 32.641456, longitude = 35.990626, display_name = ("Et Turra")}, ["Irbid"] = { latitude = 32.557292, longitude = 35.856121, display_name = ("Irbid")}, ["Tiberias"] = { latitude = 32.790725, longitude = 35.526414, display_name = ("Tiberias")}, ["Nazareth"] = { latitude = 32.704739, longitude = 35.306182, display_name = ("Nazareth")}, ["Daliyat al-Karmel"] = { latitude = 32.692722, longitude = 35.050793, display_name = ("Daliyat al-Karmel")}, ["Umm al-Fahm"] = { latitude = 32.522954, longitude = 35.151370, display_name = ("Umm al-Fahm")}, ["Afula"] = { latitude = 32.610048, longitude = 35.289515, display_name = ("Afula")}, ["Ein Harod"] = { latitude = 32.556165, longitude = 35.394209, display_name = ("Ein Harod")}, ["Beit Shean"] = { latitude = 32.492844, longitude = 35.501891, display_name = ("Beit Shean")}, ["Iksal"] = { latitude = 32.684349, longitude = 35.325562, display_name = ("Iksal")}, ["Hatzor HaGlilit"] = { latitude = 32.980146, longitude = 35.545722, display_name = ("Hatzor HaGlilit")}, ["Migdal HaEmek"] = { latitude = 32.678191, longitude = 35.241797, display_name = ("Migdal HaEmek")}, ["Nahalal"] = { latitude = 32.690694, longitude = 35.198958, display_name = ("Nahalal")}, ["Ramat Yishai"] = { latitude = 32.705166, longitude = 35.166819, display_name = ("Ramat Yishai")}, ["Kfar Yehoshua"] = { latitude = 32.681006, longitude = 35.153116, display_name = ("Kfar Yehoshua")}, ["Kiryat Tivon"] = { latitude = 32.717158, longitude = 35.126145, display_name = ("Kiryat Tivon")}, ["Yokneam Illit"] = { latitude = 32.653497, longitude = 35.102342, display_name = ("Yokneam Illit")}, ["Kafr Qara"] = { latitude = 32.504314, longitude = 35.064774, display_name = ("Kafr Qara")}, ["Pardes Hanna-Karkur"] = { latitude = 32.473446, longitude = 34.969511, display_name = ("Pardes Hanna-Karkur")}, ["Hadera"] = { latitude = 32.428035, longitude = 34.919863, display_name = ("Hadera")}, ["Isfiya"] = { latitude = 32.719767, longitude = 35.062365, display_name = ("Isfiya")}, ["Ein Hod"] = { latitude = 32.698984, longitude = 34.987603, display_name = ("Ein Hod")}, ["Atlit"] = { latitude = 32.688863, longitude = 34.943009, display_name = ("Atlit")}, ["Kiryat Motzkin"] = { latitude = 32.834316, longitude = 35.084219, display_name = ("Kiryat Motzkin")}, ["Kiryat Yam"] = { latitude = 32.842731, longitude = 35.069248, display_name = ("Kiryat Yam")}, ["Kiryat Ata"] = { latitude = 32.809126, longitude = 35.115174, display_name = ("Kiryat Ata")}, ["Shefar-Amr"] = { latitude = 32.804129, longitude = 35.172103, display_name = ("Shefar-Amr")}, ["Ibilin"] = { latitude = 32.822190, longitude = 35.192066, display_name = ("Ibilin")}, ["Kafr Manda"] = { latitude = 32.812136, longitude = 35.262557, display_name = ("Kafr Manda")}, ["Kabul"] = { latitude = 32.869646, longitude = 35.213104, display_name = ("Kabul")}, ["Tamra"] = { latitude = 32.854794, longitude = 35.194876, display_name = ("Tamra")}, ["Karmiel"] = { latitude = 32.907124, longitude = 35.286717, display_name = ("Karmiel")}, ["Deir al-Asad"] = { latitude = 32.929701, longitude = 35.275590, display_name = ("Deir al-Asad")}, ["Jadeidi Makr"] = { latitude = 32.927754, longitude = 35.148612, display_name = ("Jadeidi Makr")}, ["Yarka"] = { latitude = 32.955283, longitude = 35.211219, display_name = ("Yarka")}, ["Kafr Yasif"] = { latitude = 32.957481, longitude = 35.166921, display_name = ("Kafr Yasif")}, ["Acre"] = { latitude = 32.929521, longitude = 35.078140, display_name = ("Acre")}, ["Nahariyya"] = { latitude = 33.008845, longitude = 35.100274, display_name = ("Nahariyya")}, ["Bent Jbail"] = { latitude = 33.125874, longitude = 35.440673, display_name = ("Bent Jbail")}, ["Qiryat Shemona"] = { latitude = 33.209375, longitude = 35.570715, display_name = ("Qiryat Shemona")}, ["Tyre"] = { latitude = 33.275537, longitude = 35.215861, display_name = ("Tyre")}, ["SAA 312th Brigade%"] = { latitude = 34.588207, longitude = 36.942638, display_name = ("SAA 312th Brigade%")}, ["SAA 171th Brigade%"] = { latitude = 34.699599, longitude = 36.969931, display_name = ("SAA 171th Brigade%")}, ["Netafim military base"] = { latitude = 32.810372, longitude = 35.420586, display_name = ("Netafim military base")}, ["SyADF 24th Brigade"] = { latitude = 33.336071, longitude = 36.418180, display_name = ("SyADF 24th Brigade")}, ["SAA 158th Brigade"] = { latitude = 33.401255, longitude = 36.272973, display_name = ("SAA 158th Brigade")}, ["SAA 100th Regiment"] = { latitude = 33.380777, longitude = 36.213524, display_name = ("SAA 100th Regiment")}, ["SAA 65th Brigade"] = { latitude = 33.766740, longitude = 36.547502, display_name = ("SAA 65th Brigade")}, ["SAA 550th Brigade"] = { latitude = 34.614960, longitude = 38.254372, display_name = ("SAA 550th Brigade")}, ["Chemical Weapons Storage Area"] = { latitude = 34.627713, longitude = 38.249290, display_name = ("Chemical Weapons Storage Area")}, ["SAA 165th Brigade"] = { latitude = 33.283835, longitude = 36.255758, display_name = ("SAA 165th Brigade")}, ["Army Vehicle Training Ground"] = { latitude = 33.365284, longitude = 36.322571, display_name = ("Army Vehicle Training Ground")}, ["SAA 1st Armoured Division"] = { latitude = 33.352760, longitude = 36.296204, display_name = ("SAA 1st Armoured Division")}, ["SyADF 150th Regiment"] = { latitude = 33.251611, longitude = 36.269702, display_name = ("SyADF 150th Regiment")}, ["SAA 121th Brigade"] = { latitude = 33.278919, longitude = 36.082934, display_name = ("SAA 121th Brigade")}, ["Army Vehicle Training Ground"] = { latitude = 33.433665, longitude = 36.124922, display_name = ("Army Vehicle Training Ground")}, ["Artillery Base"] = { latitude = 33.457854, longitude = 36.128248, display_name = ("Artillery Base")}, ["SAA 155th Artillery Regiment"] = { latitude = 33.702711, longitude = 36.526507, display_name = ("SAA 155th Artillery Regiment")}, ["SAA 128th Brigade"] = { latitude = 33.971493, longitude = 36.898243, display_name = ("SAA 128th Brigade")}, ["Al Safira Military Base"] = { latitude = 36.046375, longitude = 37.334725, display_name = ("Al Safira Military Base")}, ["Military Research Base"] = { latitude = 35.983058, longitude = 37.403046, display_name = ("Military Research Base")}, ["Durayhim Military Base"] = { latitude = 35.790074, longitude = 37.728073, display_name = ("Durayhim Military Base")}, ["SAA 156th Brigade"] = { latitude = 33.667855, longitude = 36.735378, display_name = ("SAA 156th Brigade")}, ["Air Defence Academy"] = { latitude = 34.612370, longitude = 36.750060, display_name = ("Air Defence Academy")}, ["SAA 4th Armoured Division"] = { latitude = 33.506039, longitude = 36.201757, display_name = ("SAA 4th Armoured Division")}, ["Army Vehicle Training Ground"] = { latitude = 33.737184, longitude = 36.809278, display_name = ("Army Vehicle Training Ground")}, ["SAA 20th Brigade"] = { latitude = 33.773650, longitude = 36.718297, display_name = ("SAA 20th Brigade")}, ["Army Vehicle Training Ground"] = { latitude = 33.710700, longitude = 36.665598, display_name = ("Army Vehicle Training Ground")}, ["SyADF 159th Regiment"] = { latitude = 33.177072, longitude = 36.577899, display_name = ("SyADF 159th Regiment")}, ["SAA 89th Brigade"] = { latitude = 33.146138, longitude = 36.284818, display_name = ("SAA 89th Brigade")}, ["SAA 7th Division"] = { latitude = 33.456229, longitude = 36.083628, display_name = ("SAA 7th Division")}, ["Army Training Ground"] = { latitude = 33.405091, longitude = 36.319119, display_name = ("Army Training Ground")}, } veafNamedPoints._citiesMarianasIslands = { ["San Jose"] = { latitude = 14.965154, longitude = 145.628466, display_name = ("San Jose")}, ["Dandan"] = { latitude = 15.133948, longitude = 145.735471, display_name = ("Dandan")}, ["Koblerville"] = { latitude = 15.122667, longitude = 145.703809, display_name = ("Koblerville")}, ["San Vicente"] = { latitude = 15.154882, longitude = 145.739831, display_name = ("San Vicente")}, ["Chalan Kiya"] = { latitude = 15.165205, longitude = 145.720938, display_name = ("Chalan Kiya")}, ["San Jose"] = { latitude = 15.167064, longitude = 145.710962, display_name = ("San Jose")}, ["Garapan"] = { latitude = 15.206861, longitude = 145.721351, display_name = ("Garapan")}, ["Papago"] = { latitude = 15.179258, longitude = 145.752613, display_name = ("Papago")}, ["Kagman I"] = { latitude = 15.181232, longitude = 145.773957, display_name = ("Kagman I")}, ["Tanapag"] = { latitude = 15.234217, longitude = 145.747291, display_name = ("Tanapag")}, ["Puerto Rico"] = { latitude = 15.223099, longitude = 145.735812, display_name = ("Puerto Rico")}, ["San Roque"] = { latitude = 15.248449, longitude = 145.776229, display_name = ("San Roque")}, ["Songsong"] = { latitude = 14.139157, longitude = 145.139697, display_name = ("Songsong")}, ["Tuchok"] = { latitude = 14.136584, longitude = 145.154358, display_name = ("Tuchok")}, ["Sinapalo"] = { latitude = 14.166285, longitude = 145.233980, display_name = ("Sinapalo")}, ["Inarajan"] = { latitude = 13.274048, longitude = 144.746742, display_name = ("Inarajan")}, ["Pigua"] = { latitude = 13.270406, longitude = 144.671605, display_name = ("Pigua")}, ["Camp Dealy"] = { latitude = 13.353269, longitude = 144.766200, display_name = ("Camp Dealy")}, ["Yona"] = { latitude = 13.410619, longitude = 144.775485, display_name = ("Yona")}, ["Chalan Pago"] = { latitude = 13.442564, longitude = 144.768438, display_name = ("Chalan Pago")}, ["Afami"] = { latitude = 13.456341, longitude = 144.762765, display_name = ("Afami")}, ["Hagatna Heights"] = { latitude = 13.464819, longitude = 144.750032, display_name = ("Hagatna Heights")}, ["Hagatna"] = { latitude = 13.475871, longitude = 144.750834, display_name = ("Hagatna")}, ["Anigua"] = { latitude = 13.477348, longitude = 144.737995, display_name = ("Anigua")}, ["Piti"] = { latitude = 13.463047, longitude = 144.692784, display_name = ("Piti")}, ["North Tipalo"] = { latitude = 13.420384, longitude = 144.644657, display_name = ("North Tipalo")}, ["Lockwood Terrace"] = { latitude = 13.432602, longitude = 144.649979, display_name = ("Lockwood Terrace")}, ["Apra Heights"] = { latitude = 13.402864, longitude = 144.684931, display_name = ("Apra Heights")}, ["Agat"] = { latitude = 13.382019, longitude = 144.659212, display_name = ("Agat")}, ["Ladai"] = { latitude = 13.365377, longitude = 144.657204, display_name = ("Ladai")}, ["Umatac"] = { latitude = 13.298629, longitude = 144.665590, display_name = ("Umatac")}, ["Talofofo"] = { latitude = 13.354023, longitude = 144.756934, display_name = ("Talofofo")}, ["Mapas"] = { latitude = 13.473612, longitude = 144.774622, display_name = ("Mapas")}, ["Barrigada"] = { latitude = 13.465346, longitude = 144.797922, display_name = ("Barrigada")}, ["Asbeco"] = { latitude = 13.466340, longitude = 144.837012, display_name = ("Asbeco")}, ["Macheche"] = { latitude = 13.503018, longitude = 144.833579, display_name = ("Macheche")}, ["Corten Torres"] = { latitude = 13.441240, longitude = 144.805211, display_name = ("Corten Torres")}, ["Adacao"] = { latitude = 13.490890, longitude = 144.852597, display_name = ("Adacao")}, ["Apurguan"] = { latitude = 13.484178, longitude = 144.775571, display_name = ("Apurguan")}, ["Tumon"] = { latitude = 13.505412, longitude = 144.812314, display_name = ("Tumon")}, ["Diidedo"] = { latitude = 13.526543, longitude = 144.845629, display_name = ("Diidedo")}, ["Ipapao"] = { latitude = 13.524362, longitude = 144.858491, display_name = ("Ipapao")}, ["Gayinero"] = { latitude = 13.528355, longitude = 144.900124, display_name = ("Gayinero")}, ["Lupog"] = { latitude = 13.548040, longitude = 144.913590, display_name = ("Lupog")}, ["Yigo"] = { latitude = 13.539023, longitude = 144.884101, display_name = ("Yigo")}, [" Astumbo"] = { latitude = 13.554951, longitude = 144.842729, display_name = (" Astumbo")}, ["San Antonio"] = { latitude = 15.133342, longitude = 145.696097, display_name = ("San Antonio")}, ["Susupe"] = { latitude = 15.152792, longitude = 145.706059, display_name = ("Susupe")}, ["Matansa"] = { latitude = 15.251512, longitude = 145.783177, display_name = ("Matansa")}, ["Kagman II"] = { latitude = 15.176568, longitude = 145.778018, display_name = ("Kagman II")}, ["Kagman III"] = { latitude = 15.169898, longitude = 145.779899, display_name = ("Kagman III")}, ["Puetto"] = { latitude = 14.968777, longitude = 145.631754, display_name = ("Puetto")}, ["Peca"] = { latitude = 13.280913, longitude = 144.752703, display_name = ("Peca")}, ["Malojloj"] = { latitude = 13.308378, longitude = 144.761342, display_name = ("Malojloj")}, ["Tres Reyes"] = { latitude = 13.404876, longitude = 144.775087, display_name = ("Tres Reyes")}, ["Asmisen"] = { latitude = 13.400030, longitude = 144.773871, display_name = ("Asmisen")}, ["Toto"] = { latitude = 13.466524, longitude = 144.784103, display_name = ("Toto")}, ["Aspengo"] = { latitude = 13.464835, longitude = 144.798828, display_name = ("Aspengo")}, ["Asan"] = { latitude = 13.472373, longitude = 144.718320, display_name = ("Asan")}, ["Tepungan"] = { latitude = 13.467035, longitude = 144.699033, display_name = ("Tepungan")}, ["Santa Rita"] = { latitude = 13.393936, longitude = 144.662313, display_name = ("Santa Rita")}, ["Cerain"] = { latitude = 13.387143, longitude = 144.673645, display_name = ("Cerain")}, ["Rota"] = { latitude = 14.161794, longitude = 145.217911, display_name = ("Rota")}, ["Saipan"] = { latitude = 15.197839, longitude = 145.748413, display_name = ("Saipan")}, ["Tinian"] = { latitude = 15.025455, longitude = 145.625337, display_name = ("Tinian")}, ["Guam"] = { latitude = 13.506871, longitude = 144.824237, display_name = ("Guam")}, ["Faralon de Medinilla"] = { latitude = 16.019837, longitude = 146.059475, display_name = ("Faralon de Medinilla")}, ["Aguijan"] = { latitude = 14.852365, longitude = 145.558622, display_name = ("Aguijan")} } veafNamedPoints._citiesFalklands = { ["Adventure Harbour"] = { latitude = -52.141347, longitude = -59.128372, display_name = ("Adventure Harbour")}, ["Adventure Sound"] = { latitude = -52.098386, longitude = -59.026794, display_name = ("Adventure Sound")}, ["Ajax Bay"] = { latitude = -51.552937, longitude = -59.058947, display_name = ("Ajax Bay")}, ["Albemarle Harbour"] = { latitude = -52.128746, longitude = -60.555181, display_name = ("Albemarle Harbour")}, ["Albemarle Sealing Station"] = { latitude = -52.191718, longitude = -60.541186, display_name = ("Albemarle Sealing Station")}, ["Albemarle Station Farm"] = { latitude = -52.192037, longitude = -60.541448, display_name = ("Albemarle Station Farm")}, ["Anchor Inlet"] = { latitude = -52.163469, longitude = -60.675087, display_name = ("Anchor Inlet")}, ["Anthony Creek"] = { latitude = -51.991294, longitude = -60.704667, display_name = ("Anthony Creek")}, ["Arch Road"] = { latitude = -52.217481, longitude = -60.481882, display_name = ("Arch Road")}, ["Archer Cove"] = { latitude = -51.900713, longitude = -58.621588, display_name = ("Archer Cove")}, ["Arrow Harbour"] = { latitude = -51.870955, longitude = -58.938896, display_name = ("Arrow Harbour")}, ["Arrow Point"] = { latitude = -51.658696, longitude = -57.787496, display_name = ("Arrow Point")}, ["Arrow Point"] = { latitude = -51.390883, longitude = -60.309993, display_name = ("Arrow Point")}, ["Bagwell Point"] = { latitude = -52.199496, longitude = -59.037158, display_name = ("Bagwell Point")}, ["Bald Road"] = { latitude = -51.788896, longitude = -60.934811, display_name = ("Bald Road")}, ["Barrow Harbour"] = { latitude = -52.137979, longitude = -59.122324, display_name = ("Barrow Harbour")}, ["Barthe Creek"] = { latitude = -51.569221, longitude = -58.128761, display_name = ("Barthe Creek")}, ["Bay of Harbours"] = { latitude = -52.201295, longitude = -59.306774, display_name = ("Bay of Harbours")}, ["Bay Point"] = { latitude = -51.322665, longitude = -59.779232, display_name = ("Bay Point")}, ["Beach Point"] = { latitude = -51.754544, longitude = -57.967429, display_name = ("Beach Point")}, ["Beacon Point"] = { latitude = -51.779966, longitude = -60.976640, display_name = ("Beacon Point")}, ["Beatrice Cove"] = { latitude = -51.652524, longitude = -57.750449, display_name = ("Beatrice Cove")}, ["Beaver Bay"] = { latitude = -51.822272, longitude = -61.268050, display_name = ("Beaver Bay")}, ["Beaver Farm"] = { latitude = -51.853341, longitude = -61.253730, display_name = ("Beaver Farm")}, ["Beaver Harbour"] = { latitude = -51.828630, longitude = -61.232042, display_name = ("Beaver Harbour")}, ["Bense Harbour"] = { latitude = -51.491511, longitude = -60.502610, display_name = ("Bense Harbour")}, ["Berkeley Sound"] = { latitude = -51.567041, longitude = -57.920952, display_name = ("Berkeley Sound")}, ["Black Bog Hill"] = { latitude = -51.350456, longitude = -60.707095, display_name = ("Black Bog Hill")}, ["Black Point"] = { latitude = -51.332928, longitude = -58.378547, display_name = ("Black Point")}, ["Black Point"] = { latitude = -52.182544, longitude = -59.644732, display_name = ("Black Point")}, ["Black Point"] = { latitude = -51.636176, longitude = -57.741265, display_name = ("Black Point")}, ["Black Rock"] = { latitude = -51.782947, longitude = -58.667048, display_name = ("Black Rock")}, ["Blanco Bay"] = { latitude = -51.667205, longitude = -57.801819, display_name = ("Blanco Bay")}, ["Bleaker Farm"] = { latitude = -52.207270, longitude = -58.851203, display_name = ("Bleaker Farm")}, ["Blue Beach Farm"] = { latitude = -51.573737, longitude = -59.035232, display_name = ("Blue Beach Farm")}, ["Bluff Cove"] = { latitude = -51.744749, longitude = -58.163363, display_name = ("Bluff Cove")}, ["Bluff Cove Farm"] = { latitude = -51.747866, longitude = -58.180226, display_name = ("Bluff Cove Farm")}, ["Bluff Creek"] = { latitude = -51.884591, longitude = -58.738339, display_name = ("Bluff Creek")}, ["Bluff Head"] = { latitude = -52.082930, longitude = -58.866933, display_name = ("Bluff Head")}, ["Bluff Point"] = { latitude = -51.346054, longitude = -60.242318, display_name = ("Bluff Point")}, ["Boat Point"] = { latitude = -51.846095, longitude = -58.215147, display_name = ("Boat Point")}, ["Bodie Inlet"] = { latitude = -51.845592, longitude = -59.089907, display_name = ("Bodie Inlet")}, ["Bold Cove"] = { latitude = -51.592340, longitude = -59.449896, display_name = ("Bold Cove")}, ["Bold Point"] = { latitude = -51.345160, longitude = -59.922759, display_name = ("Bold Point")}, ["Bold Point"] = { latitude = -52.179212, longitude = -59.406037, display_name = ("Bold Point")}, ["Bold Point"] = { latitude = -51.631464, longitude = -59.464877, display_name = ("Bold Point")}, ["Bold Point"] = { latitude = -51.689927, longitude = -61.217581, display_name = ("Bold Point")}, ["Bold Point"] = { latitude = -51.454087, longitude = -58.455141, display_name = ("Bold Point")}, ["Bold Point"] = { latitude = -51.672801, longitude = -60.367404, display_name = ("Bold Point")}, ["Bold Point"] = { latitude = -51.770471, longitude = -58.064755, display_name = ("Bold Point")}, ["Bonners Bay"] = { latitude = -51.578570, longitude = -59.031708, display_name = ("Bonners Bay")}, ["Bougainville Creek"] = { latitude = -51.531834, longitude = -58.102505, display_name = ("Bougainville Creek")}, ["Boulder Point"] = { latitude = -51.876981, longitude = -61.240990, display_name = ("Boulder Point")}, ["Boundary Farm"] = { latitude = -51.501099, longitude = -60.167983, display_name = ("Boundary Farm")}, ["Bouy Beach"] = { latitude = -51.621968, longitude = -60.438983, display_name = ("Bouy Beach")}, ["Boxwood Gulch"] = { latitude = -51.606642, longitude = -60.457182, display_name = ("Boxwood Gulch")}, ["Boxwood Point"] = { latitude = -51.438688, longitude = -60.598869, display_name = ("Boxwood Point")}, ["Boxwood Point"] = { latitude = -51.347510, longitude = -60.679375, display_name = ("Boxwood Point")}, ["Brasse Mar"] = { latitude = -51.582116, longitude = -58.201780, display_name = ("Brasse Mar")}, ["Brenton Loch"] = { latitude = -51.788436, longitude = -59.011480, display_name = ("Brenton Loch")}, ["Brett Harbour"] = { latitude = -51.370588, longitude = -60.157485, display_name = ("Brett Harbour")}, ["Brookfield Farm"] = { latitude = -51.547650, longitude = -58.202158, display_name = ("Brookfield Farm")}, ["Brown Harbour"] = { latitude = -51.819877, longitude = -60.311422, display_name = ("Brown Harbour")}, ["Brown Point"] = { latitude = -51.624484, longitude = -60.255375, display_name = ("Brown Point")}, ["Bull Cove"] = { latitude = -52.341695, longitude = -59.331846, display_name = ("Bull Cove")}, ["Bull Point"] = { latitude = -52.331261, longitude = -59.305723, display_name = ("Bull Point")}, ["Bull Point"] = { latitude = -52.116830, longitude = -59.075182, display_name = ("Bull Point")}, ["Bull Point"] = { latitude = -51.820633, longitude = -58.299768, display_name = ("Bull Point")}, ["Bull Road"] = { latitude = -52.313220, longitude = -59.350524, display_name = ("Bull Road")}, ["Burnt Harbour"] = { latitude = -51.408084, longitude = -60.132833, display_name = ("Burnt Harbour")}, ["Button Bay"] = { latitude = -51.897906, longitude = -58.642911, display_name = ("Button Bay")}, ["Byron Sound"] = { latitude = -51.312728, longitude = -60.635335, display_name = ("Byron Sound")}, ["Byron Sound"] = { latitude = -51.434668, longitude = -60.278292, display_name = ("Byron Sound")}, ["Calf Creek"] = { latitude = -51.953000, longitude = -58.798946, display_name = ("Calf Creek")}, ["Calm Head"] = { latitude = -52.115282, longitude = -60.927773, display_name = ("Calm Head")}, ["Camilla Creek"] = { latitude = -51.773596, longitude = -58.958377, display_name = ("Camilla Creek")}, ["Campa Menta Bay"] = { latitude = -51.381192, longitude = -58.136919, display_name = ("Campa Menta Bay")}, ["Canard Cove"] = { latitude = -51.546029, longitude = -58.148442, display_name = ("Canard Cove")}, ["Caneja Creek"] = { latitude = -51.528266, longitude = -58.276632, display_name = ("Caneja Creek")}, ["Caneja Creek"] = { latitude = -51.852958, longitude = -58.268201, display_name = ("Caneja Creek")}, ["Cape Bougainville"] = { latitude = -51.297778, longitude = -58.461667, display_name = ("Cape Bougainville")}, ["Cape Carysfort"] = { latitude = -51.414145, longitude = -57.857702, display_name = ("Cape Carysfort")}, ["Cape Dolphin"] = { latitude = -51.233056, longitude = -58.965556, display_name = ("Cape Dolphin")}, ["Cape Dolphin Farm"] = { latitude = -51.340202, longitude = -58.841209, display_name = ("Cape Dolphin Farm")}, ["Cape Frehel"] = { latitude = -51.383332, longitude = -58.232592, display_name = ("Cape Frehel")}, ["Cape Meredith"] = { latitude = -52.256569, longitude = -60.647004, display_name = ("Cape Meredith")}, ["Cape Orford"] = { latitude = -52.012676, longitude = -61.070166, display_name = ("Cape Orford")}, ["Cape Pembroke"] = { latitude = -51.682622, longitude = -57.719467, display_name = ("Cape Pembroke")}, ["Cape Percival"] = { latitude = -51.829890, longitude = -61.342871, display_name = ("Cape Percival")}, ["Cape Split"] = { latitude = -51.808953, longitude = -61.321392, display_name = ("Cape Split")}, ["Cape Tamar"] = { latitude = -51.279334, longitude = -59.496009, display_name = ("Cape Tamar")}, ["Cape Terrible"] = { latitude = -51.328255, longitude = -60.729069, display_name = ("Cape Terrible")}, ["Carcass Bay"] = { latitude = -51.969211, longitude = -59.910014, display_name = ("Carcass Bay")}, ["Carcass Farm"] = { latitude = -51.290598, longitude = -60.555728, display_name = ("Carcass Farm")}, ["Careening Cove"] = { latitude = -51.511908, longitude = -58.968086, display_name = ("Careening Cove")}, ["Carew Harbour"] = { latitude = -52.002942, longitude = -60.675095, display_name = ("Carew Harbour")}, ["Carruther Point"] = { latitude = -51.356703, longitude = -59.949984, display_name = ("Carruther Point")}, ["Cat Cove"] = { latitude = -51.339848, longitude = -60.692468, display_name = ("Cat Cove")}, ["Cattle Point"] = { latitude = -51.797170, longitude = -58.117697, display_name = ("Cattle Point")}, ["Cattle Point"] = { latitude = -52.180656, longitude = -59.316613, display_name = ("Cattle Point")}, ["Chabot Creek"] = { latitude = -51.499486, longitude = -58.052144, display_name = ("Chabot Creek")}, ["Chabot Point"] = { latitude = -51.521119, longitude = -58.045393, display_name = ("Chabot Point")}, ["Chaffers Gullet"] = { latitude = -52.128539, longitude = -60.381927, display_name = ("Chaffers Gullet")}, ["Chancho Point"] = { latitude = -51.497271, longitude = -59.127860, display_name = ("Chancho Point")}, ["Channel Point"] = { latitude = -51.408586, longitude = -60.080028, display_name = ("Channel Point")}, ["Charles Point"] = { latitude = -51.657462, longitude = -57.773559, display_name = ("Charles Point")}, ["Charles Point"] = { latitude = -51.654294, longitude = -57.774345, display_name = ("Charles Point")}, ["Chatham Harbour"] = { latitude = -51.844408, longitude = -60.968499, display_name = ("Chatham Harbour")}, ["Choiseul Sound"] = { latitude = -51.919920, longitude = -58.667164, display_name = ("Choiseul Sound")}, ["Christina Bay"] = { latitude = -51.689036, longitude = -57.725301, display_name = ("Christina Bay")}, ["Christmas Harbour"] = { latitude = -51.676497, longitude = -60.175563, display_name = ("Christmas Harbour")}, ["Cinnamon Valley"] = { latitude = -51.615123, longitude = -60.439326, display_name = ("Cinnamon Valley")}, ["Cliff Point"] = { latitude = -51.304943, longitude = -60.087286, display_name = ("Cliff Point")}, ["Cliff Point"] = { latitude = -52.009645, longitude = -58.521316, display_name = ("Cliff Point")}, ["Committee Bay"] = { latitude = -51.329808, longitude = -59.928579, display_name = ("Committee Bay")}, ["Concordia Bay"] = { latitude = -51.308316, longitude = -58.603670, display_name = ("Concordia Bay")}, ["Cow Bay"] = { latitude = -51.434360, longitude = -57.860742, display_name = ("Cow Bay")}, ["Cow Point"] = { latitude = -52.222534, longitude = -59.222787, display_name = ("Cow Point")}, ["Cracker Point"] = { latitude = -51.623180, longitude = -60.446547, display_name = ("Cracker Point")}, ["Creek Point"] = { latitude = -51.391317, longitude = -59.861594, display_name = ("Creek Point")}, ["Crooked Inlet"] = { latitude = -51.547676, longitude = -60.317216, display_name = ("Crooked Inlet")}, ["Crooked Inlet Farm"] = { latitude = -51.584687, longitude = -60.211575, display_name = ("Crooked Inlet Farm")}, ["Cutter Cove"] = { latitude = -51.756514, longitude = -59.251089, display_name = ("Cutter Cove")}, ["Cygnet Harbour"] = { latitude = -51.889092, longitude = -59.485505, display_name = ("Cygnet Harbour")}, ["Dangerous Point"] = { latitude = -52.004271, longitude = -58.350184, display_name = ("Dangerous Point")}, ["Danson Harbour"] = { latitude = -52.105342, longitude = -59.640060, display_name = ("Danson Harbour")}, ["Darwin Harbour"] = { latitude = -51.819754, longitude = -58.949755, display_name = ("Darwin Harbour")}, ["Deaths Head"] = { latitude = -51.412424, longitude = -60.646141, display_name = ("Deaths Head")}, ["Dick Point"] = { latitude = -51.798325, longitude = -60.526310, display_name = ("Dick Point")}, ["Dick Point"] = { latitude = -51.805960, longitude = -60.538446, display_name = ("Dick Point")}, ["Doctor Point"] = { latitude = -51.662827, longitude = -57.818266, display_name = ("Doctor Point")}, ["Doctors Head"] = { latitude = -51.517838, longitude = -59.060551, display_name = ("Doctors Head")}, ["Dog Point"] = { latitude = -51.917862, longitude = -58.887792, display_name = ("Dog Point")}, ["Don Carlos Bay"] = { latitude = -51.635786, longitude = -57.751898, display_name = ("Don Carlos Bay")}, ["Double Creek"] = { latitude = -52.007008, longitude = -60.585358, display_name = ("Double Creek")}, ["Driftwood Point"] = { latitude = -52.257556, longitude = -59.024863, display_name = ("Driftwood Point")}, ["Duclos Point"] = { latitude = -51.534758, longitude = -58.023005, display_name = ("Duclos Point")}, ["Dunbar Creek"] = { latitude = -51.408398, longitude = -60.449023, display_name = ("Dunbar Creek")}, ["Dunbar Farm"] = { latitude = -51.412911, longitude = -60.453126, display_name = ("Dunbar Farm")}, ["Dunnose Head"] = { latitude = -51.680273, longitude = -60.644741, display_name = ("Dunnose Head")}, ["Dunnose Head Farm"] = { latitude = -51.756294, longitude = -60.419300, display_name = ("Dunnose Head Farm")}, ["Duperrey Harbour"] = { latitude = -51.570575, longitude = -58.110766, display_name = ("Duperrey Harbour")}, ["Dyke Point"] = { latitude = -51.930136, longitude = -61.077890, display_name = ("Dyke Point")}, ["Eagle Point"] = { latitude = -51.539609, longitude = -57.769933, display_name = ("Eagle Point")}, ["East Bay"] = { latitude = -51.762900, longitude = -60.244217, display_name = ("East Bay")}, ["East Branch"] = { latitude = -51.577950, longitude = -60.283935, display_name = ("East Branch")}, ["East Cove"] = { latitude = -51.906194, longitude = -58.434863, display_name = ("East Cove")}, ["East Head"] = { latitude = -51.998721, longitude = -60.012603, display_name = ("East Head")}, ["East Point"] = { latitude = -51.627038, longitude = -60.417544, display_name = ("East Point")}, ["East Point"] = { latitude = -51.336376, longitude = -60.048802, display_name = ("East Point")}, ["East Road"] = { latitude = -51.792227, longitude = -58.089330, display_name = ("East Road")}, ["Eddy Point"] = { latitude = -51.757431, longitude = -61.278680, display_name = ("Eddy Point")}, ["Egg Harbour"] = { latitude = -51.838668, longitude = -59.384837, display_name = ("Egg Harbour")}, ["Elephant Bay"] = { latitude = -52.338976, longitude = -59.770069, display_name = ("Elephant Bay")}, ["Elephant Bay"] = { latitude = -51.295372, longitude = -59.569987, display_name = ("Elephant Bay")}, ["Elephant Beach Farm"] = { latitude = -51.386471, longitude = -58.761120, display_name = ("Elephant Beach Farm")}, ["Elephant Cove"] = { latitude = -51.801556, longitude = -60.919018, display_name = ("Elephant Cove")}, ["Elephant Island"] = { latitude = -51.876758, longitude = -58.291741, display_name = ("Elephant Island")}, ["Elephant Point"] = { latitude = -51.264560, longitude = -60.321271, display_name = ("Elephant Point")}, ["Elephant Point"] = { latitude = -51.884396, longitude = -58.302410, display_name = ("Elephant Point")}, ["Eliza Cove"] = { latitude = -51.733162, longitude = -57.832224, display_name = ("Eliza Cove")}, ["Enderby Point"] = { latitude = -52.027125, longitude = -58.550929, display_name = ("Enderby Point")}, ["Engineer Point"] = { latitude = -51.682155, longitude = -57.832434, display_name = ("Engineer Point")}, ["Entrance Cove"] = { latitude = -51.958204, longitude = -60.582047, display_name = ("Entrance Cove")}, ["Estancia Farm"] = { latitude = -51.656414, longitude = -58.158132, display_name = ("Estancia Farm")}, ["Evelyn Station Farm"] = { latitude = -51.557936, longitude = -58.438389, display_name = ("Evelyn Station Farm")}, ["Fairy Cove"] = { latitude = -51.682310, longitude = -57.881263, display_name = ("Fairy Cove")}, ["Fall Point"] = { latitude = -51.355221, longitude = -59.930109, display_name = ("Fall Point")}, ["False Creek"] = { latitude = -51.947274, longitude = -58.720797, display_name = ("False Creek")}, ["Fanning Harbour"] = { latitude = -51.476264, longitude = -59.087048, display_name = ("Fanning Harbour")}, ["Fanning Head"] = { latitude = -51.468689, longitude = -59.133956, display_name = ("Fanning Head")}, ["Fanny Road"] = { latitude = -52.228709, longitude = -59.355979, display_name = ("Fanny Road")}, ["Fegen Inlet"] = { latitude = -52.069906, longitude = -60.877425, display_name = ("Fegen Inlet")}, ["Fern Valley Creek"] = { latitude = -51.559577, longitude = -59.032180, display_name = ("Fern Valley Creek")}, ["Findlay Creek"] = { latitude = -51.865220, longitude = -59.007858, display_name = ("Findlay Creek")}, ["Findlay Harbour"] = { latitude = -51.991712, longitude = -59.610271, display_name = ("Findlay Harbour")}, ["First Sand Grass Bay"] = { latitude = -51.410798, longitude = -58.287214, display_name = ("First Sand Grass Bay")}, ["Fish Creek"] = { latitude = -52.033380, longitude = -60.266662, display_name = ("Fish Creek")}, ["Fish Creek"] = { latitude = -51.851089, longitude = -61.238351, display_name = ("Fish Creek")}, ["Fitz Cove"] = { latitude = -51.757362, longitude = -58.184152, display_name = ("Fitz Cove")}, ["Fitzroy Basin"] = { latitude = -51.748116, longitude = -58.163295, display_name = ("Fitzroy Basin")}, ["Fitzroy Creek"] = { latitude = -51.801795, longitude = -58.233783, display_name = ("Fitzroy Creek")}, ["Flores Harbour"] = { latitude = -52.221937, longitude = -59.589586, display_name = ("Flores Harbour")}, ["Foot Point"] = { latitude = -51.486594, longitude = -60.156605, display_name = ("Foot Point")}, ["Foul Bay"] = { latitude = -51.356882, longitude = -58.907793, display_name = ("Foul Bay")}, ["Fox Bay"] = { latitude = -51.980544, longitude = -60.054567, display_name = ("Fox Bay")}, ["Fox Harbour"] = { latitude = -52.090174, longitude = -59.113212, display_name = ("Fox Harbour")}, ["Fox Point"] = { latitude = -51.921434, longitude = -58.404849, display_name = ("Fox Point")}, ["Fox Point West"] = { latitude = -51.484590, longitude = -60.066399, display_name = ("Fox Point West")}, ["French Harbour"] = { latitude = -51.851182, longitude = -61.102910, display_name = ("French Harbour")}, ["Garden Point"] = { latitude = -51.765499, longitude = -58.199429, display_name = ("Garden Point")}, ["Garibaldi Bay"] = { latitude = -51.923011, longitude = -60.884712, display_name = ("Garibaldi Bay")}, ["Garibaldi Point"] = { latitude = -51.917258, longitude = -60.872606, display_name = ("Garibaldi Point")}, ["Gascoigne Point"] = { latitude = -51.336667, longitude = -59.920724, display_name = ("Gascoigne Point")}, ["George Island Farm"] = { latitude = -52.354310, longitude = -59.750933, display_name = ("George Island Farm")}, ["Gibraltar Reef"] = { latitude = -51.297752, longitude = -60.841599, display_name = ("Gibraltar Reef")}, ["Gibraltar Station Farm"] = { latitude = -51.435519, longitude = -58.372301, display_name = ("Gibraltar Station Farm")}, ["Gladys Cove"] = { latitude = -51.964385, longitude = -60.767355, display_name = ("Gladys Cove")}, ["Glen Point"] = { latitude = -51.344044, longitude = -59.988903, display_name = ("Glen Point")}, ["Goat Point"] = { latitude = -51.419880, longitude = -58.295414, display_name = ("Goat Point")}, ["Golding Bay"] = { latitude = -51.337682, longitude = -59.800150, display_name = ("Golding Bay")}, ["Golding Island Farm"] = { latitude = -51.365733, longitude = -59.688851, display_name = ("Golding Island Farm")}, ["Goose Green Farm"] = { latitude = -51.827197, longitude = -58.973479, display_name = ("Goose Green Farm")}, ["Gosling Creek"] = { latitude = -51.995770, longitude = -58.483548, display_name = ("Gosling Creek")}, ["Grantham Sound"] = { latitude = -51.662760, longitude = -59.094429, display_name = ("Grantham Sound")}, ["Grave Cove"] = { latitude = -51.363813, longitude = -60.632306, display_name = ("Grave Cove")}, ["Grave Point"] = { latitude = -51.534778, longitude = -58.022050, display_name = ("Grave Point")}, ["Green Rincon"] = { latitude = -51.259715, longitude = -59.730316, display_name = ("Green Rincon")}, ["Greenfield Farm"] = { latitude = -51.566702, longitude = -58.913984, display_name = ("Greenfield Farm")}, ["Gull Harbour"] = { latitude = -51.899290, longitude = -60.891627, display_name = ("Gull Harbour")}, ["Gull Point"] = { latitude = -51.902582, longitude = -60.871708, display_name = ("Gull Point")}, ["Gull Point"] = { latitude = -51.305412, longitude = -60.041420, display_name = ("Gull Point")}, ["Gypsy Cove"] = { latitude = -51.675343, longitude = -57.807646, display_name = ("Gypsy Cove")}, ["Hadassa Bay"] = { latitude = -51.679617, longitude = -57.822095, display_name = ("Hadassa Bay")}, ["Halfway Cove"] = { latitude = -51.747143, longitude = -60.409237, display_name = ("Halfway Cove")}, ["Halfway Cove"] = { latitude = -52.213418, longitude = -59.688356, display_name = ("Halfway Cove")}, ["Hamblin Cove"] = { latitude = -51.665914, longitude = -57.829628, display_name = ("Hamblin Cove")}, ["Hamond Cove"] = { latitude = -51.970619, longitude = -58.619442, display_name = ("Hamond Cove")}, ["Head of the Bay Creek"] = { latitude = -51.595324, longitude = -59.036449, display_name = ("Head of the Bay Creek")}, ["Head of the Bay Farm"] = { latitude = -51.601000, longitude = -59.028733, display_name = ("Head of the Bay Farm")}, ["Hearnden Water"] = { latitude = -51.666680, longitude = -57.870111, display_name = ("Hearnden Water")}, ["Hells Kitchen"] = { latitude = -51.649646, longitude = -57.771212, display_name = ("Hells Kitchen")}, ["High Bluff Cape"] = { latitude = -52.077307, longitude = -61.019633, display_name = ("High Bluff Cape")}, ["Hill Cove"] = { latitude = -51.499126, longitude = -60.100880, display_name = ("Hill Cove")}, ["Hill Gap"] = { latitude = -51.808568, longitude = -59.753351, display_name = ("Hill Gap")}, ["Home Farm"] = { latitude = -51.459496, longitude = -58.608190, display_name = ("Home Farm")}, ["Hookers Point"] = { latitude = -51.699380, longitude = -57.776114, display_name = ("Hookers Point")}, ["Hope Cottage Farm"] = { latitude = -51.476205, longitude = -58.627879, display_name = ("Hope Cottage Farm")}, ["Hope Harbour"] = { latitude = -51.354444, longitude = -60.635519, display_name = ("Hope Harbour")}, ["Hope Place"] = { latitude = -51.845482, longitude = -59.018050, display_name = ("Hope Place")}, ["Hope Point"] = { latitude = -51.340919, longitude = -60.663009, display_name = ("Hope Point")}, ["Hope Reef"] = { latitude = -51.188422, longitude = -60.748644, display_name = ("Hope Reef")}, ["Horse Point"] = { latitude = -51.720542, longitude = -57.825481, display_name = ("Horse Point")}, ["Horseshoe Bay"] = { latitude = -51.501874, longitude = -58.295734, display_name = ("Horseshoe Bay")}, ["Horseshoe Bay Farm"] = { latitude = -51.500600, longitude = -58.282186, display_name = ("Horseshoe Bay Farm")}, ["Hospital Point"] = { latitude = -51.506539, longitude = -59.040613, display_name = ("Hospital Point")}, ["Hoste Inlet"] = { latitude = -52.122396, longitude = -60.719805, display_name = ("Hoste Inlet")}, ["House Bay"] = { latitude = -51.615731, longitude = -60.430427, display_name = ("House Bay")}, ["House Cove"] = { latitude = -51.979270, longitude = -60.834174, display_name = ("House Cove")}, ["Hummock Point"] = { latitude = -51.369627, longitude = -59.661486, display_name = ("Hummock Point")}, ["Hut Point"] = { latitude = -51.391855, longitude = -58.281720, display_name = ("Hut Point")}, ["Inlet Point"] = { latitude = -52.032347, longitude = -60.896101, display_name = ("Inlet Point")}, ["Island Creek"] = { latitude = -51.966375, longitude = -58.690896, display_name = ("Island Creek")}, ["Island Harbour"] = { latitude = -51.826817, longitude = -58.328615, display_name = ("Island Harbour")}, ["Isthmus Cove"] = { latitude = -51.702373, longitude = -60.318353, display_name = ("Isthmus Cove")}, ["Jenesta Point"] = { latitude = -51.263972, longitude = -59.604113, display_name = ("Jenesta Point")}, ["Jersey Harbour"] = { latitude = -51.460572, longitude = -59.281497, display_name = ("Jersey Harbour")}, ["Jersey Point"] = { latitude = -51.437514, longitude = -59.203734, display_name = ("Jersey Point")}, ["John Point"] = { latitude = -51.878341, longitude = -58.810311, display_name = ("John Point")}, ["Johnson Harbour"] = { latitude = -51.518490, longitude = -58.014982, display_name = ("Johnson Harbour")}, ["Justice Inlet"] = { latitude = -51.317746, longitude = -59.933861, display_name = ("Justice Inlet")}, ["Kelp Bay"] = { latitude = -51.997446, longitude = -58.502669, display_name = ("Kelp Bay")}, ["Kelp Creek"] = { latitude = -51.878802, longitude = -61.035726, display_name = ("Kelp Creek")}, ["Kelp Harbour"] = { latitude = -51.783958, longitude = -59.314842, display_name = ("Kelp Harbour")}, ["Kelp Point"] = { latitude = -51.968219, longitude = -60.039339, display_name = ("Kelp Point")}, ["Kelp Point"] = { latitude = -51.860206, longitude = -58.214683, display_name = ("Kelp Point")}, ["Keppel Sound"] = { latitude = -51.275503, longitude = -59.914230, display_name = ("Keppel Sound")}, ["Kidney Cove"] = { latitude = -51.629979, longitude = -57.751222, display_name = ("Kidney Cove")}, ["King George Bay"] = { latitude = -51.639195, longitude = -60.487091, display_name = ("King George Bay")}, ["Kingsford Valley Farm"] = { latitude = -51.575244, longitude = -59.030547, display_name = ("Kingsford Valley Farm")}, ["Kits Creek"] = { latitude = -52.181281, longitude = -60.646999, display_name = ("Kits Creek")}, ["Knob Point"] = { latitude = -51.990574, longitude = -58.604448, display_name = ("Knob Point")}, ["Lafonia"] = { latitude = -51.963629, longitude = -59.269508, display_name = ("Lafonia")}, ["Laguna Algas"] = { latitude = -52.338435, longitude = -59.374844, display_name = ("Laguna Algas")}, ["Lake Point"] = { latitude = -51.723313, longitude = -57.855957, display_name = ("Lake Point")}, ["Lamarche Point"] = { latitude = -51.529779, longitude = -57.977386, display_name = ("Lamarche Point")}, ["Lancaster Point"] = { latitude = -51.320139, longitude = -59.916086, display_name = ("Lancaster Point")}, ["Landsend Bluff"] = { latitude = -51.692622, longitude = -61.332394, display_name = ("Landsend Bluff")}, ["Lebon Creek"] = { latitude = -51.567057, longitude = -58.115262, display_name = ("Lebon Creek")}, ["Leicester Creek"] = { latitude = -51.945932, longitude = -60.319490, display_name = ("Leicester Creek")}, ["Leicester Creek Farm"] = { latitude = -51.894767, longitude = -60.282105, display_name = ("Leicester Creek Farm")}, ["Letterbox Point"] = { latitude = -51.388649, longitude = -60.045455, display_name = ("Letterbox Point")}, ["Limpet Creek"] = { latitude = -51.349780, longitude = -58.605237, display_name = ("Limpet Creek")}, ["Lion Point"] = { latitude = -51.332123, longitude = -60.710610, display_name = ("Lion Point")}, ["Lion Point"] = { latitude = -51.366640, longitude = -59.833243, display_name = ("Lion Point")}, ["Lion Point"] = { latitude = -51.300788, longitude = -58.582513, display_name = ("Lion Point")}, ["Lion Point"] = { latitude = -51.418767, longitude = -60.409144, display_name = ("Lion Point")}, ["Lively Farm"] = { latitude = -51.994056, longitude = -58.464810, display_name = ("Lively Farm")}, ["Long Creek"] = { latitude = -52.013724, longitude = -58.519495, display_name = ("Long Creek")}, ["Long Island Farm"] = { latitude = -51.576691, longitude = -58.072443, display_name = ("Long Island Farm")}, ["Loop Head"] = { latitude = -51.763743, longitude = -60.898901, display_name = ("Loop Head")}, ["Lorenzo Farm"] = { latitude = -51.458481, longitude = -58.610466, display_name = ("Lorenzo Farm")}, ["Low Bay"] = { latitude = -52.070327, longitude = -58.799429, display_name = ("Low Bay")}, ["Low Bay"] = { latitude = -52.087312, longitude = -58.924913, display_name = ("Low Bay")}, ["Low Point"] = { latitude = -51.953245, longitude = -58.699222, display_name = ("Low Point")}, ["Low Point"] = { latitude = -51.682924, longitude = -60.135142, display_name = ("Low Point")}, ["Low Point"] = { latitude = -52.080310, longitude = -59.100845, display_name = ("Low Point")}, ["Lucas Bay"] = { latitude = -52.161806, longitude = -60.428802, display_name = ("Lucas Bay")}, ["Lucas Point"] = { latitude = -52.170535, longitude = -60.371744, display_name = ("Lucas Point")}, ["MacBride Head"] = { latitude = -51.363595, longitude = -57.947688, display_name = ("MacBride Head")}, ["Mackinnon Creek"] = { latitude = -51.888239, longitude = -58.642407, display_name = ("Mackinnon Creek")}, ["Magellan Cove"] = { latitude = -51.507884, longitude = -58.000527, display_name = ("Magellan Cove")}, ["Magellan Point"] = { latitude = -51.508729, longitude = -58.017204, display_name = ("Magellan Point")}, ["Main Point"] = { latitude = -51.423793, longitude = -59.733845, display_name = ("Main Point")}, ["Main Point Farm"] = { latitude = -51.435452, longitude = -59.910888, display_name = ("Main Point Farm")}, ["Malo Creek"] = { latitude = -51.988144, longitude = -60.200014, display_name = ("Malo Creek")}, ["Mare Harbour"] = { latitude = -51.899019, longitude = -58.499322, display_name = ("Mare Harbour")}, ["Mare Rincon"] = { latitude = -51.788602, longitude = -60.556644, display_name = ("Mare Rincon")}, ["Mark Point"] = { latitude = -51.889140, longitude = -60.873914, display_name = ("Mark Point")}, ["Marville Bay"] = { latitude = -51.381511, longitude = -58.190567, display_name = ("Marville Bay")}, ["McGill Shoals"] = { latitude = -51.215896, longitude = -60.548830, display_name = ("McGill Shoals")}, ["Mengeary Point"] = { latitude = -51.644166, longitude = -57.729877, display_name = ("Mengeary Point")}, ["Middle Bay"] = { latitude = -51.414759, longitude = -59.037867, display_name = ("Middle Bay")}, ["Middle Creek"] = { latitude = -51.523077, longitude = -58.273340, display_name = ("Middle Creek")}, ["Middle Point"] = { latitude = -51.386296, longitude = -59.004149, display_name = ("Middle Point")}, ["Miles Creek"] = { latitude = -51.926883, longitude = -58.855010, display_name = ("Miles Creek")}, ["Misery Valley"] = { latitude = -51.370012, longitude = -60.701308, display_name = ("Misery Valley")}, ["Moffit Bay"] = { latitude = -52.229469, longitude = -59.045205, display_name = ("Moffit Bay")}, ["Moffitt Harbour"] = { latitude = -52.086561, longitude = -59.608255, display_name = ("Moffitt Harbour")}, ["Monkey Point"] = { latitude = -51.539112, longitude = -57.956153, display_name = ("Monkey Point")}, ["Moss Side Farm"] = { latitude = -51.480737, longitude = -58.921770, display_name = ("Moss Side Farm")}, ["Mound Point"] = { latitude = -51.911615, longitude = -60.498308, display_name = ("Mound Point")}, ["Mound Point"] = { latitude = -51.965423, longitude = -60.838165, display_name = ("Mound Point")}, ["Mount Kent Farm"] = { latitude = -51.558594, longitude = -58.133369, display_name = ("Mount Kent Farm")}, ["Muddy Creek"] = { latitude = -51.404714, longitude = -58.550541, display_name = ("Muddy Creek")}, ["Murrell Farm"] = { latitude = -51.649688, longitude = -57.915065, display_name = ("Murrell Farm")}, ["Narrow Creek"] = { latitude = -51.984336, longitude = -60.630289, display_name = ("Narrow Creek")}, ["New Haven"] = { latitude = -51.728040, longitude = -59.213422, display_name = ("New Haven")}, ["New Haven"] = { latitude = -51.730937, longitude = -59.206936, display_name = ("New Haven")}, ["New Year Cove"] = { latitude = -51.941506, longitude = -60.902967, display_name = ("New Year Cove")}, ["North Arm"] = { latitude = -52.132566, longitude = -59.366813, display_name = ("North Arm")}, ["North Arm Farm"] = { latitude = -52.123686, longitude = -59.367667, display_name = ("North Arm Farm")}, ["North Basin"] = { latitude = -51.739801, longitude = -58.059073, display_name = ("North Basin")}, ["North Bluff"] = { latitude = -51.676951, longitude = -61.263334, display_name = ("North Bluff")}, ["North Creek"] = { latitude = -51.504976, longitude = -58.298178, display_name = ("North Creek")}, ["North East Creek"] = { latitude = -51.536379, longitude = -58.228826, display_name = ("North East Creek")}, ["North East Point"] = { latitude = -51.788059, longitude = -58.110879, display_name = ("North East Point")}, ["North Harbour"] = { latitude = -51.704812, longitude = -61.239800, display_name = ("North Harbour")}, ["North Head"] = { latitude = -52.013191, longitude = -60.236063, display_name = ("North Head")}, ["North Point"] = { latitude = -51.284847, longitude = -59.959585, display_name = ("North Point")}, ["North Point"] = { latitude = -52.133682, longitude = -58.826962, display_name = ("North Point")}, ["North West Bay"] = { latitude = -51.726630, longitude = -60.351934, display_name = ("North West Bay")}, ["North West Point"] = { latitude = -51.246296, longitude = -60.600007, display_name = ("North West Point")}, ["North West Rincon"] = { latitude = -51.584008, longitude = -59.174866, display_name = ("North West Rincon")}, ["Northwest Bay"] = { latitude = -51.607022, longitude = -60.446616, display_name = ("Northwest Bay")}, ["Norton Inlet"] = { latitude = -51.848392, longitude = -58.852891, display_name = ("Norton Inlet")}, ["Old House Creek"] = { latitude = -51.532889, longitude = -59.054260, display_name = ("Old House Creek")}, ["Ordnance Point"] = { latitude = -51.682707, longitude = -57.826334, display_name = ("Ordnance Point")}, ["Pea Point"] = { latitude = -52.184780, longitude = -60.721951, display_name = ("Pea Point")}, ["Peat Bog Point"] = { latitude = -52.359314, longitude = -59.811794, display_name = ("Peat Bog Point")}, ["Pebble Cove"] = { latitude = -51.258457, longitude = -59.826415, display_name = ("Pebble Cove")}, ["Pebble Sound"] = { latitude = -51.357472, longitude = -59.512424, display_name = ("Pebble Sound")}, ["Pebbly Bay"] = { latitude = -52.211735, longitude = -58.847901, display_name = ("Pebbly Bay")}, ["Penarrow Point"] = { latitude = -51.390205, longitude = -60.312211, display_name = ("Penarrow Point")}, ["Penarrow Point"] = { latitude = -51.684214, longitude = -57.866423, display_name = ("Penarrow Point")}, ["Penguin Cove"] = { latitude = -51.961060, longitude = -60.933952, display_name = ("Penguin Cove")}, ["Penguin Point"] = { latitude = -52.017793, longitude = -58.445828, display_name = ("Penguin Point")}, ["Penguin Point"] = { latitude = -51.378005, longitude = -60.643930, display_name = ("Penguin Point")}, ["Penguin Point"] = { latitude = -51.925935, longitude = -60.645309, display_name = ("Penguin Point")}, ["Petrel City"] = { latitude = -51.610911, longitude = -60.455966, display_name = ("Petrel City")}, ["Phillips Bay"] = { latitude = -52.169471, longitude = -59.762535, display_name = ("Phillips Bay")}, ["Phillips Point"] = { latitude = -51.412471, longitude = -58.531530, display_name = ("Phillips Point")}, ["Phillips Point"] = { latitude = -52.141146, longitude = -59.779412, display_name = ("Phillips Point")}, ["Phillips Point"] = { latitude = -51.717188, longitude = -57.815852, display_name = ("Phillips Point")}, ["Pickthorne Point"] = { latitude = -51.491082, longitude = -60.482486, display_name = ("Pickthorne Point")}, ["Pillar Bluff"] = { latitude = -51.929733, longitude = -61.096022, display_name = ("Pillar Bluff")}, ["Pillar Cove"] = { latitude = -51.919742, longitude = -61.080251, display_name = ("Pillar Cove")}, ["Pitt Creek"] = { latitude = -51.835184, longitude = -61.057570, display_name = ("Pitt Creek")}, ["Platt Point"] = { latitude = -51.485101, longitude = -58.371542, display_name = ("Platt Point")}, ["Platt Point Rincon"] = { latitude = -51.483051, longitude = -58.362437, display_name = ("Platt Point Rincon")}, ["Plaza Creek"] = { latitude = -51.389593, longitude = -58.503270, display_name = ("Plaza Creek")}, ["Pleasant Point"] = { latitude = -51.816171, longitude = -58.166513, display_name = ("Pleasant Point")}, ["Pleasant Road"] = { latitude = -51.834253, longitude = -58.220286, display_name = ("Pleasant Road")}, ["Point Frances"] = { latitude = -51.306076, longitude = -59.926413, display_name = ("Point Frances")}, ["Point Frio"] = { latitude = -51.469817, longitude = -58.438331, display_name = ("Point Frio")}, ["Poke Point"] = { latitude = -51.603985, longitude = -59.395480, display_name = ("Poke Point")}, ["Poker Point"] = { latitude = -51.572567, longitude = -59.065837, display_name = ("Poker Point")}, ["Pond Bay"] = { latitude = -51.874822, longitude = -60.455360, display_name = ("Pond Bay")}, ["Porpoise Cove"] = { latitude = -51.963995, longitude = -60.740833, display_name = ("Porpoise Cove")}, ["Porpoise Point"] = { latitude = -52.349135, longitude = -59.310699, display_name = ("Porpoise Point")}, ["Port Albemarle"] = { latitude = -52.189141, longitude = -60.433045, display_name = ("Port Albemarle")}, ["Port Edgar"] = { latitude = -52.007587, longitude = -60.258315, display_name = ("Port Edgar")}, ["Port Edgar Farm"] = { latitude = -52.049719, longitude = -60.280796, display_name = ("Port Edgar Farm")}, ["Port Egmont"] = { latitude = -51.357740, longitude = -60.079165, display_name = ("Port Egmont")}, ["Port Egmont"] = { latitude = -51.358531, longitude = -60.028546, display_name = ("Port Egmont")}, ["Port Fitzroy"] = { latitude = -51.776292, longitude = -58.122988, display_name = ("Port Fitzroy")}, ["Port Harriet"] = { latitude = -51.731387, longitude = -57.930307, display_name = ("Port Harriet")}, ["Port Howard"] = { latitude = -51.641454, longitude = -59.549389, display_name = ("Port Howard")}, ["Port Howard Farm"] = { latitude = -51.611011, longitude = -59.520974, display_name = ("Port Howard Farm")}, ["Port King"] = { latitude = -51.911053, longitude = -59.538088, display_name = ("Port King")}, ["Port Louis Farm"] = { latitude = -51.529193, longitude = -58.128300, display_name = ("Port Louis Farm")}, ["Port Louis Harbour"] = { latitude = -51.530323, longitude = -58.121733, display_name = ("Port Louis Harbour")}, ["Port North"] = { latitude = -51.471765, longitude = -60.449009, display_name = ("Port North")}, ["Port North Farm"] = { latitude = -51.487490, longitude = -60.354387, display_name = ("Port North Farm")}, ["Port Philomel"] = { latitude = -51.733673, longitude = -60.280781, display_name = ("Port Philomel")}, ["Port Pleasant"] = { latitude = -51.776405, longitude = -58.178043, display_name = ("Port Pleasant")}, ["Port Purvis"] = { latitude = -51.432715, longitude = -59.461441, display_name = ("Port Purvis")}, ["Port Richards"] = { latitude = -51.957966, longitude = -60.470295, display_name = ("Port Richards")}, ["Port Salvador"] = { latitude = -51.519052, longitude = -58.386283, display_name = ("Port Salvador")}, ["Port Stephens"] = { latitude = -52.126928, longitude = -60.792761, display_name = ("Port Stephens")}, ["Port Stephens Farm"] = { latitude = -52.097294, longitude = -60.831988, display_name = ("Port Stephens Farm")}, ["Port Sussex"] = { latitude = -51.659800, longitude = -59.008681, display_name = ("Port Sussex")}, ["Port Sussex Farm"] = { latitude = -51.655203, longitude = -59.015105, display_name = ("Port Sussex Farm")}, ["Port William"] = { latitude = -51.665188, longitude = -57.765262, display_name = ("Port William")}, ["Porvenir Gulch"] = { latitude = -51.616643, longitude = -60.434527, display_name = ("Porvenir Gulch")}, ["Praltos Point"] = { latitude = -51.723094, longitude = -59.197469, display_name = ("Praltos Point")}, ["Prong Point"] = { latitude = -52.096086, longitude = -58.419714, display_name = ("Prong Point")}, ["Punta Ave Marina"] = { latitude = -51.401776, longitude = -59.496803, display_name = ("Punta Ave Marina")}, ["Punta Redonda"] = { latitude = -51.752796, longitude = -60.361038, display_name = ("Punta Redonda")}, ["Punta Turano"] = { latitude = -51.519542, longitude = -59.337199, display_name = ("Punta Turano")}, ["Punte Triste"] = { latitude = -52.139807, longitude = -58.658876, display_name = ("Punte Triste")}, ["Purvis Point"] = { latitude = -51.402686, longitude = -59.467578, display_name = ("Purvis Point")}, ["Pyramid Cove"] = { latitude = -52.009196, longitude = -58.600495, display_name = ("Pyramid Cove")}, ["Pyramid Point"] = { latitude = -52.013822, longitude = -58.592663, display_name = ("Pyramid Point")}, ["Quaker Harbour"] = { latitude = -51.812733, longitude = -61.079292, display_name = ("Quaker Harbour")}, ["Queen Charlotte Bay"] = { latitude = -51.846903, longitude = -60.698646, display_name = ("Queen Charlotte Bay")}, ["Queen Point"] = { latitude = -51.931631, longitude = -60.585415, display_name = ("Queen Point")}, ["Rabbit Cove"] = { latitude = -51.666001, longitude = -57.781735, display_name = ("Rabbit Cove")}, ["Rabbit Cove"] = { latitude = -51.428835, longitude = -59.643831, display_name = ("Rabbit Cove")}, ["Rabbit Point"] = { latitude = -51.301898, longitude = -59.712549, display_name = ("Rabbit Point")}, ["Rabbit Rincon"] = { latitude = -51.379738, longitude = -58.062168, display_name = ("Rabbit Rincon")}, ["Race Point"] = { latitude = -51.983479, longitude = -60.995536, display_name = ("Race Point")}, ["Race Point"] = { latitude = -51.413260, longitude = -59.097487, display_name = ("Race Point")}, ["Race Point Farm"] = { latitude = -51.502265, longitude = -58.995508, display_name = ("Race Point Farm")}, ["Rain Cove"] = { latitude = -51.966659, longitude = -58.646178, display_name = ("Rain Cove")}, ["Rame Head"] = { latitude = -51.413434, longitude = -60.240043, display_name = ("Rame Head")}, ["Rapid Point"] = { latitude = -51.382959, longitude = -60.005486, display_name = ("Rapid Point")}, ["Red Hill Point"] = { latitude = -51.554681, longitude = -59.075482, display_name = ("Red Hill Point")}, ["Red Point"] = { latitude = -51.381679, longitude = -60.082017, display_name = ("Red Point")}, ["Reef Point"] = { latitude = -52.094952, longitude = -58.460020, display_name = ("Reef Point")}, ["Reef Point"] = { latitude = -51.316063, longitude = -59.905550, display_name = ("Reef Point")}, ["Rincon de Saino"] = { latitude = -51.615658, longitude = -58.255112, display_name = ("Rincon de Saino")}, ["Rincon Grande Estate Farm"] = { latitude = -51.461580, longitude = -58.309318, display_name = ("Rincon Grande Estate Farm")}, ["River Harbour"] = { latitude = -51.440768, longitude = -59.723396, display_name = ("River Harbour")}, ["Riverside Farm"] = { latitude = -51.751296, longitude = -58.353514, display_name = ("Riverside Farm")}, ["Riverside Farm"] = { latitude = -51.752827, longitude = -58.464821, display_name = ("Riverside Farm")}, ["Riverview Farm"] = { latitude = -51.630201, longitude = -58.320929, display_name = ("Riverview Farm")}, ["Robinson Point"] = { latitude = -51.286762, longitude = -59.988474, display_name = ("Robinson Point")}, ["Rock Harbour"] = { latitude = -51.383419, longitude = -59.797468, display_name = ("Rock Harbour")}, ["Rocky Inlet"] = { latitude = -51.696315, longitude = -60.060110, display_name = ("Rocky Inlet")}, ["Rodney Bluff"] = { latitude = -52.074534, longitude = -61.018839, display_name = ("Rodney Bluff")}, ["Rodney Cove"] = { latitude = -52.060192, longitude = -61.005482, display_name = ("Rodney Cove")}, ["Ronda House"] = { latitude = -51.369944, longitude = -58.359965, display_name = ("Ronda House")}, ["Rookery Bay"] = { latitude = -51.705132, longitude = -57.796626, display_name = ("Rookery Bay")}, ["Round Point"] = { latitude = -51.761193, longitude = -60.323406, display_name = ("Round Point")}, ["Ruggles Bay"] = { latitude = -52.062724, longitude = -59.657641, display_name = ("Ruggles Bay")}, ["Sal Point"] = { latitude = -52.058210, longitude = -58.528891, display_name = ("Sal Point")}, ["Saladero Farm"] = { latitude = -51.722688, longitude = -59.061739, display_name = ("Saladero Farm")}, ["Salvador Water"] = { latitude = -51.456596, longitude = -58.415279, display_name = ("Salvador Water")}, ["San Carlos Water"] = { latitude = -51.497988, longitude = -59.079257, display_name = ("San Carlos Water")}, ["Sand Bay"] = { latitude = -51.728844, longitude = -57.948579, display_name = ("Sand Bay")}, ["Sand Bay"] = { latitude = -51.552580, longitude = -57.990131, display_name = ("Sand Bay")}, ["Sand Paddock"] = { latitude = -51.334160, longitude = -60.720062, display_name = ("Sand Paddock")}, ["Sandy Bay"] = { latitude = -52.187524, longitude = -58.837839, display_name = ("Sandy Bay")}, ["Sandy Cove"] = { latitude = -52.053058, longitude = -59.144211, display_name = ("Sandy Cove")}, ["Saturday Point"] = { latitude = -52.029294, longitude = -59.099526, display_name = ("Saturday Point")}, ["Sea Lion Cove"] = { latitude = -51.627591, longitude = -60.419411, display_name = ("Sea Lion Cove")}, ["Sea Lion Point"] = { latitude = -51.355033, longitude = -58.342606, display_name = ("Sea Lion Point")}, ["Seablite Point"] = { latitude = -51.604880, longitude = -60.458946, display_name = ("Seablite Point")}, ["Seal Bay"] = { latitude = -51.378751, longitude = -58.011522, display_name = ("Seal Bay")}, ["Seal Cove"] = { latitude = -52.028603, longitude = -58.659652, display_name = ("Seal Cove")}, ["Seal Point"] = { latitude = -51.737879, longitude = -57.845177, display_name = ("Seal Point")}, ["Seal Point"] = { latitude = -52.102030, longitude = -59.070403, display_name = ("Seal Point")}, ["Sealer Cove"] = { latitude = -51.802352, longitude = -60.865985, display_name = ("Sealer Cove")}, ["Sealer Cove"] = { latitude = -51.364252, longitude = -60.077974, display_name = ("Sealer Cove")}, ["Seaview Creek"] = { latitude = -51.583900, longitude = -59.076281, display_name = ("Seaview Creek")}, ["Seaview Point"] = { latitude = -51.580231, longitude = -59.059531, display_name = ("Seaview Point")}, ["Second Sand Grass Bay"] = { latitude = -51.399848, longitude = -58.280358, display_name = ("Second Sand Grass Bay")}, ["Semaphore Point"] = { latitude = -52.166661, longitude = -59.659719, display_name = ("Semaphore Point")}, ["Settlement Creek"] = { latitude = -52.056713, longitude = -60.270653, display_name = ("Settlement Creek")}, ["Shag Cove"] = { latitude = -51.738039, longitude = -59.644865, display_name = ("Shag Cove")}, ["Shag Harbour"] = { latitude = -51.736809, longitude = -59.618897, display_name = ("Shag Harbour")}, ["Shag Point"] = { latitude = -52.222292, longitude = -59.672306, display_name = ("Shag Point")}, ["Shallop Point"] = { latitude = -51.700088, longitude = -60.093706, display_name = ("Shallop Point")}, ["Shallow Bay"] = { latitude = -51.404147, longitude = -59.990658, display_name = ("Shallow Bay")}, ["Shallow Bay"] = { latitude = -51.675373, longitude = -60.103057, display_name = ("Shallow Bay")}, ["Shallow Bay Farm"] = { latitude = -51.423212, longitude = -59.996788, display_name = ("Shallow Bay Farm")}, ["Shallow Bluff"] = { latitude = -51.774074, longitude = -60.590136, display_name = ("Shallow Bluff")}, ["Shallow Cove"] = { latitude = -51.780765, longitude = -58.220458, display_name = ("Shallow Cove")}, ["Shallow Harbour"] = { latitude = -51.998549, longitude = -58.433919, display_name = ("Shallow Harbour")}, ["Shallow Harbour"] = { latitude = -51.777055, longitude = -60.508747, display_name = ("Shallow Harbour")}, ["Shallow Harbour Farm"] = { latitude = -51.746787, longitude = -60.523139, display_name = ("Shallow Harbour Farm")}, ["Sheffield Farm"] = { latitude = -51.656998, longitude = -60.189851, display_name = ("Sheffield Farm")}, ["Shell Bay"] = { latitude = -52.074099, longitude = -58.955000, display_name = ("Shell Bay")}, ["Shell Point"] = { latitude = -52.096211, longitude = -58.987538, display_name = ("Shell Point")}, ["Ship Harbour"] = { latitude = -51.711248, longitude = -61.269775, display_name = ("Ship Harbour")}, ["Ship Harbour"] = { latitude = -51.329505, longitude = -59.468029, display_name = ("Ship Harbour")}, ["Ship Point"] = { latitude = -52.100205, longitude = -59.093109, display_name = ("Ship Point")}, ["Shivery Creek"] = { latitude = -52.156968, longitude = -60.711010, display_name = ("Shivery Creek")}, ["Skull Bay"] = { latitude = -51.885657, longitude = -61.127629, display_name = ("Skull Bay")}, ["Smokey Point"] = { latitude = -51.609499, longitude = -60.429868, display_name = ("Smokey Point")}, ["Smylie Creek"] = { latitude = -51.431899, longitude = -58.967657, display_name = ("Smylie Creek")}, ["Snug Cove"] = { latitude = -52.186721, longitude = -59.421272, display_name = ("Snug Cove")}, ["Sound Point"] = { latitude = -51.438062, longitude = -60.098868, display_name = ("Sound Point")}, ["South East Point"] = { latitude = -51.796862, longitude = -58.117493, display_name = ("South East Point")}, ["South Harbour"] = { latitude = -51.734138, longitude = -61.287317, display_name = ("South Harbour")}, ["South Harbour"] = { latitude = -52.021379, longitude = -60.863314, display_name = ("South Harbour")}, ["South Harbour Farm"] = { latitude = -52.005681, longitude = -60.745499, display_name = ("South Harbour Farm")}, ["South Head"] = { latitude = -52.018077, longitude = -60.246792, display_name = ("South Head")}, ["Southwest Bay"] = { latitude = -51.621380, longitude = -60.443805, display_name = ("Southwest Bay")}, ["Sparrow Cove"] = { latitude = -51.650953, longitude = -57.808256, display_name = ("Sparrow Cove")}, ["Spring Point Farm"] = { latitude = -51.830975, longitude = -60.465875, display_name = ("Spring Point Farm")}, ["Squib Point"] = { latitude = -51.834199, longitude = -58.932243, display_name = ("Squib Point")}, ["Stanley Harbour"] = { latitude = -51.686850, longitude = -57.837498, display_name = ("Stanley Harbour")}, ["Starfish Creek"] = { latitude = -52.016717, longitude = -60.266876, display_name = ("Starfish Creek")}, ["States Cove"] = { latitude = -51.835617, longitude = -60.901174, display_name = ("States Cove")}, ["Steep Point"] = { latitude = -51.716846, longitude = -60.286209, display_name = ("Steep Point")}, ["Steeple Jason East"] = { latitude = -51.048500, longitude = -61.190185, display_name = ("Steeple Jason East")}, ["Steeple Jason West"] = { latitude = -51.024536, longitude = -61.235246, display_name = ("Steeple Jason West")}, ["Stevelly Bay"] = { latitude = -51.443777, longitude = -60.464029, display_name = ("Stevelly Bay")}, ["Stinker Point"] = { latitude = -51.821081, longitude = -61.198139, display_name = ("Stinker Point")}, ["Stoney Ridge Farm"] = { latitude = -52.015289, longitude = -60.489749, display_name = ("Stoney Ridge Farm")}, ["Strike Off Point"] = { latitude = -51.589443, longitude = -57.975369, display_name = ("Strike Off Point")}, ["Sulivan Harbour"] = { latitude = -52.046051, longitude = -59.176741, display_name = ("Sulivan Harbour")}, ["Surf Bay"] = { latitude = -51.695176, longitude = -57.774713, display_name = ("Surf Bay")}, ["Swan Inlet Farm"] = { latitude = -51.828249, longitude = -58.621617, display_name = ("Swan Inlet Farm")}, ["Swan Point"] = { latitude = -51.765078, longitude = -60.865615, display_name = ("Swan Point")}, ["Symonds Harbour"] = { latitude = -51.832967, longitude = -60.352449, display_name = ("Symonds Harbour")}, ["Table Cove"] = { latitude = -51.809716, longitude = -61.245239, display_name = ("Table Cove")}, ["Table Point"] = { latitude = -51.797256, longitude = -61.231860, display_name = ("Table Point")}, ["Tamar Point"] = { latitude = -51.343849, longitude = -59.400786, display_name = ("Tamar Point")}, ["Tank Point"] = { latitude = -52.293205, longitude = -59.743459, display_name = ("Tank Point")}, ["Tea Point"] = { latitude = -52.350651, longitude = -59.658444, display_name = ("Tea Point")}, ["Teal Creek"] = { latitude = -51.819781, longitude = -58.924652, display_name = ("Teal Creek")}, ["Teal River Farm"] = { latitude = -51.656837, longitude = -60.077527, display_name = ("Teal River Farm")}, ["Tern Point"] = { latitude = -51.954475, longitude = -60.845847, display_name = ("Tern Point")}, ["Terra Motas Point"] = { latitude = -51.653839, longitude = -59.045895, display_name = ("Terra Motas Point")}, ["The Canache"] = { latitude = -51.696076, longitude = -57.790580, display_name = ("The Canache")}, ["The Careenage"] = { latitude = -51.535925, longitude = -58.108857, display_name = ("The Careenage")}, ["The Moro"] = { latitude = -51.426845, longitude = -58.524650, display_name = ("The Moro")}, ["The Narrows"] = { latitude = -51.682131, longitude = -57.829232, display_name = ("The Narrows")}, ["The Waterfall"] = { latitude = -51.368482, longitude = -60.687188, display_name = ("The Waterfall")}, ["Town Point"] = { latitude = -51.658161, longitude = -60.241948, display_name = ("Town Point")}, ["Tussac Point"] = { latitude = -51.682907, longitude = -57.817987, display_name = ("Tussac Point")}, ["Tussac Point"] = { latitude = -51.871245, longitude = -60.473567, display_name = ("Tussac Point")}, ["Tussac Point"] = { latitude = -52.347217, longitude = -59.377799, display_name = ("Tussac Point")}, ["Uranie Bay"] = { latitude = -51.580497, longitude = -58.037639, display_name = ("Uranie Bay")}, ["Useless Bay"] = { latitude = -51.831826, longitude = -60.938501, display_name = ("Useless Bay")}, ["Useless Water"] = { latitude = -52.015760, longitude = -58.438264, display_name = ("Useless Water")}, ["Victoria Harbour"] = { latitude = -51.931896, longitude = -58.829985, display_name = ("Victoria Harbour")}, ["Volunteer Lagoon"] = { latitude = -51.503374, longitude = -57.862759, display_name = ("Volunteer Lagoon")}, ["Volunteer Point"] = { latitude = -51.512270, longitude = -57.741694, display_name = ("Volunteer Point")}, ["Watering Cove"] = { latitude = -51.940323, longitude = -60.430641, display_name = ("Watering Cove")}, ["Weddell Farm"] = { latitude = -51.893838, longitude = -60.910683, display_name = ("Weddell Farm")}, ["Weddell Point"] = { latitude = -51.903560, longitude = -61.135711, display_name = ("Weddell Point")}, ["Weir Creek"] = { latitude = -51.662754, longitude = -57.868638, display_name = ("Weir Creek")}, ["West Arm"] = { latitude = -52.191772, longitude = -60.526042, display_name = ("West Arm")}, ["West Bluff"] = { latitude = -51.378002, longitude = -60.704999, display_name = ("West Bluff")}, ["West Cove"] = { latitude = -51.801623, longitude = -60.389571, display_name = ("West Cove")}, ["West Cove"] = { latitude = -51.893768, longitude = -58.544670, display_name = ("West Cove")}, ["West Head"] = { latitude = -52.002374, longitude = -60.104839, display_name = ("West Head")}, ["West Point"] = { latitude = -51.359234, longitude = -59.830078, display_name = ("West Point")}, ["Westley Farm"] = { latitude = -51.559167, longitude = -60.245445, display_name = ("Westley Farm")}, ["Westpoint Cove"] = { latitude = -51.350966, longitude = -60.680022, display_name = ("Westpoint Cove")}, ["Whale Bay"] = { latitude = -51.384049, longitude = -59.448223, display_name = ("Whale Bay")}, ["Whale Point"] = { latitude = -51.873196, longitude = -58.247151, display_name = ("Whale Point")}, ["Whalebone Cove"] = { latitude = -51.687674, longitude = -57.805515, display_name = ("Whalebone Cove")}, ["Whaler Bay"] = { latitude = -51.537127, longitude = -60.481153, display_name = ("Whaler Bay")}, ["Wharton Harbour"] = { latitude = -51.966096, longitude = -59.585808, display_name = ("Wharton Harbour")}, ["White Bluff"] = { latitude = -51.895517, longitude = -58.750505, display_name = ("White Bluff")}, ["White Cliff Point"] = { latitude = -51.905011, longitude = -59.593413, display_name = ("White Cliff Point")}, ["White Rock Bay"] = { latitude = -51.437584, longitude = -59.245148, display_name = ("White Rock Bay")}, ["White Rock Point"] = { latitude = -51.400605, longitude = -59.195913, display_name = ("White Rock Point")}, ["Whitsand Bay"] = { latitude = -51.717405, longitude = -60.201817, display_name = ("Whitsand Bay")}, ["Windy Valley"] = { latitude = -51.618994, longitude = -60.437008, display_name = ("Windy Valley")}, ["Wine Bay"] = { latitude = -52.263220, longitude = -59.503219, display_name = ("Wine Bay")}, ["Wine Bluff"] = { latitude = -51.964795, longitude = -61.052753, display_name = ("Wine Bluff")}, ["Wood Cove"] = { latitude = -51.904341, longitude = -61.103511, display_name = ("Wood Cove")}, ["Wood Cove"] = { latitude = -52.114175, longitude = -60.828552, display_name = ("Wood Cove")}, ["Woods Valley"] = { latitude = -51.611764, longitude = -60.447296, display_name = ("Woods Valley")}, ["Woolly Gut Point"] = { latitude = -51.351663, longitude = -60.665646, display_name = ("Woolly Gut Point")}, ["Wreck Point"] = { latitude = -51.278634, longitude = -59.545552, display_name = ("Wreck Point")}, ["Wreck Point Farm"] = { latitude = -51.577344, longitude = -59.083134, display_name = ("Wreck Point Farm")}, ["Yeguada"] = { latitude = -51.861793, longitude = -59.417379, display_name = ("Yeguada")}, ["Yorke Bay"] = { latitude = -51.679262, longitude = -57.800362, display_name = ("Yorke Bay")}, ["The Sound"] = { latitude = -51.730407, longitude = -59.430426, display_name = ("The Sound")}, ["The Sound"] = { latitude = -51.658177, longitude = -59.321324, display_name = ("The Sound")}, ["Mount Kent"] = { latitude = -51.670161, longitude = -58.101211, display_name = ("Mount Kent")}, ["Mount Challenger"] = { latitude = -51.699074, longitude = -58.126390, display_name = ("Mount Challenger")}, ["Mount Usborne"] = { latitude = -51.703101, longitude = -58.834935, display_name = ("Mount Usborne")}, ["No Mans Land"] = { latitude = -51.689332, longitude = -58.688344, display_name = ("No Mans Land")}, ["Wickham Heights"] = { latitude = -51.699126, longitude = -58.419443, display_name = ("Wickham Heights")}, ["Salvador"] = { latitude = -51.434775, longitude = -58.370467, display_name = ("Salvador")}, ["Foam Creek"] = { latitude = -51.461585, longitude = -58.308689, display_name = ("Foam Creek")}, ["Port Louis"] = { latitude = -51.529323, longitude = -58.128474, display_name = ("Port Louis")}, ["Fitzroy"] = { latitude = -51.784321, longitude = -58.228791, display_name = ("Fitzroy")}, ["Le Marchand"] = { latitude = -50.745431, longitude = -69.476891, display_name = ("Le Marchand")}, ["Mata Amarilla"] = { latitude = -49.583809, longitude = -71.179944, display_name = ("Mata Amarilla")}, ["Mata Grande"] = { latitude = -48.855507, longitude = -67.582036, display_name = ("Mata Grande")}, ["Miranda"] = { latitude = -53.683333, longitude = -68.116668, display_name = ("Miranda")}, ["Monte Aymond"] = { latitude = -52.102844, longitude = -69.533337, display_name = ("Monte Aymond")}, ["Monte Dinero"] = { latitude = -52.323463, longitude = -68.559501, display_name = ("Monte Dinero")}, ["Paso Gobernador Gregores"] = { latitude = -47.833333, longitude = -66.583334, display_name = ("Paso Gobernador Gregores")}, ["Paso Gregores"] = { latitude = -47.800000, longitude = -66.583334, display_name = ("Paso Gregores")}, ["Paso Rodolfo Roballos"] = { latitude = -47.133332, longitude = -71.850023, display_name = ("Paso Rodolfo Roballos")}, ["Puerto Bandera"] = { latitude = -50.313724, longitude = -72.791908, display_name = ("Puerto Bandera")}, ["Puerto Coig"] = { latitude = -50.954045, longitude = -69.220979, display_name = ("Puerto Coig")}, ["Estancia Harberton"] = { latitude = -54.877762, longitude = -67.329386, display_name = ("Estancia Harberton")}, ["Puerto Remolino"] = { latitude = -54.860454, longitude = -67.859145, display_name = ("Puerto Remolino")}, ["Puerto San Julian"] = { latitude = -49.306953, longitude = -67.729787, display_name = ("Puerto San Julian")}, ["Punta del Lago"] = { latitude = -49.595274, longitude = -72.057822, display_name = ("Punta del Lago")}, ["Punta del Monte"] = { latitude = -51.634492, longitude = -71.312863, display_name = ("Punta del Monte")}, ["Rincan"] = { latitude = -50.294210, longitude = -69.918910, display_name = ("Rincan")}, ["Estancia San Justo"] = { latitude = -54.028494, longitude = -68.544922, display_name = ("Estancia San Justo")}, ["Estancia Sara"] = { latitude = -53.433583, longitude = -68.183738, display_name = ("Estancia Sara")}, ["Sarmiento"] = { latitude = -54.337442, longitude = -68.180968, display_name = ("Sarmiento")}, ["Estancia Despedida"] = { latitude = -53.952200, longitude = -68.256912, display_name = ("Estancia Despedida")}, ["Tamel Aike"] = { latitude = -48.316667, longitude = -70.966680, display_name = ("Tamel Aike")}, ["Estancia Tapi Aike"] = { latitude = -51.056127, longitude = -71.794226, display_name = ("Estancia Tapi Aike")}, ["Tellier"] = { latitude = -47.648895, longitude = -66.040225, display_name = ("Tellier")}, ["Tres Lagos"] = { latitude = -49.598080, longitude = -71.446487, display_name = ("Tres Lagos")}, ["Tucu Tucu"] = { latitude = -48.433333, longitude = -71.850020, display_name = ("Tucu Tucu")}, ["Ushuaia"] = { latitude = -54.806933, longitude = -68.307325, display_name = ("Ushuaia")}, ["Veintiocho de Noviembre"] = { latitude = -51.579567, longitude = -72.211488, display_name = ("Veintiocho de Noviembre")}, ["Estancia Viamonte"] = { latitude = -53.995575, longitude = -67.414225, display_name = ("Estancia Viamonte")}, ["Vieja Carmen"] = { latitude = -54.498210, longitude = -67.767154, display_name = ("Vieja Carmen")}, ["Antonio de Biedma"] = { latitude = -47.487031, longitude = -66.503434, display_name = ("Antonio de Biedma")}, ["Bajo Caracoles"] = { latitude = -47.443328, longitude = -70.926533, display_name = ("Bajo Caracoles")}, ["Bajo Picaso"] = { latitude = -49.051259, longitude = -68.387156, display_name = ("Bajo Picaso")}, ["Cancha Carrera"] = { latitude = -51.256817, longitude = -72.220102, display_name = ("Cancha Carrera")}, ["Cauchicol"] = { latitude = -53.881017, longitude = -67.929884, display_name = ("Cauchicol")}, ["Charles Fuhr"] = { latitude = -50.270052, longitude = -71.887286, display_name = ("Charles Fuhr")}, ["Comandante Portillo"] = { latitude = -54.900000, longitude = -66.000000, display_name = ("Comandante Portillo")}, ["Corpen"] = { latitude = -49.583333, longitude = -69.500005, display_name = ("Corpen")}, ["Estancia Cullen"] = { latitude = -52.883340, longitude = -68.446516, display_name = ("Estancia Cullen")}, ["Diego Ritchie"] = { latitude = -51.978050, longitude = -70.396159, display_name = ("Diego Ritchie")}, ["Rospentek"] = { latitude = -51.665372, longitude = -72.145268, display_name = ("Rospentek")}, ["Flecha Negra"] = { latitude = -47.833333, longitude = -69.666674, display_name = ("Flecha Negra")}, ["Florida Negra"] = { latitude = -48.422050, longitude = -67.354710, display_name = ("Florida Negra")}, ["Fuentes del Coyle"] = { latitude = -51.029367, longitude = -71.481186, display_name = ("Fuentes del Coyle")}, ["Gobernador Gregores"] = { latitude = -48.750870, longitude = -70.248552, display_name = ("Gobernador Gregores")}, ["Gobernador Moyano"] = { latitude = -47.774568, longitude = -68.582995, display_name = ("Gobernador Moyano")}, ["Gobernador Moyano"] = { latitude = -47.850000, longitude = -66.633334, display_name = ("Gobernador Moyano")}, ["Gobernador Moyano"] = { latitude = -51.863858, longitude = -70.559914, display_name = ("Gobernador Moyano")}, ["Guankenken Aike"] = { latitude = -51.429320, longitude = -69.827830, display_name = ("Guankenken Aike")}, ["Hill Station"] = { latitude = -51.575508, longitude = -69.232891, display_name = ("Hill Station")}, ["La Aragonesa"] = { latitude = -48.075173, longitude = -69.761083, display_name = ("La Aragonesa")}, ["La Bajada"] = { latitude = -48.883333, longitude = -69.700006, display_name = ("La Bajada")}, ["Lago Cardiel"] = { latitude = -48.947044, longitude = -71.376108, display_name = ("Lago Cardiel")}, ["Laguna Grande"] = { latitude = -49.506762, longitude = -70.159896, display_name = ("Laguna Grande")}, ["Lai Aike"] = { latitude = -49.400000, longitude = -68.750004, display_name = ("Lai Aike")}, ["La Marina"] = { latitude = -54.316667, longitude = -68.550002, display_name = ("La Marina")}, ["Lapataia"] = { latitude = -54.855622, longitude = -68.577236, display_name = ("Lapataia")}, ["Las Horquetas"] = { latitude = -48.254585, longitude = -71.162442, display_name = ("Las Horquetas")}, ["Las Perdices"] = { latitude = -50.316667, longitude = -71.250012, display_name = ("Las Perdices")}, ["Tierra del Fuego"] = { latitude = -53.890464, longitude = -68.348600, display_name = ("Tierra del Fuego")}, ["Lago Posadas"] = { latitude = -47.565618, longitude = -71.742692, display_name = ("Lago Posadas")}, ["Tierra del Fuego"] = { latitude = -54.407107, longitude = -67.897491, display_name = ("Tierra del Fuego")}, ["Santa Cruz"] = { latitude = -48.569333, longitude = -70.160685, display_name = ("Santa Cruz")}, ["Punta Azconape"] = { latitude = -49.253917, longitude = -67.631239, display_name = ("Punta Azconape")}, ["Punta Gallows"] = { latitude = -49.310372, longitude = -67.695671, display_name = ("Punta Gallows")}, ["Cerro Castillo"] = { latitude = -51.256249, longitude = -72.344706, display_name = ("Cerro Castillo")}, ["Punta Loyola"] = { latitude = -51.608878, longitude = -69.011639, display_name = ("Punta Loyola")}, ["Calluqueo"] = { latitude = -47.657245, longitude = -72.477491, display_name = ("Calluqueo")}, ["La Juanita"] = { latitude = -47.336737, longitude = -72.174784, display_name = ("La Juanita")}, ["La Peligrosa"] = { latitude = -47.380286, longitude = -72.247977, display_name = ("La Peligrosa")}, ["Lago Escondido"] = { latitude = -54.640774, longitude = -67.762884, display_name = ("Lago Escondido")}, ["Aeropuerto Viejo"] = { latitude = -50.338096, longitude = -72.246806, display_name = ("Aeropuerto Viejo")}, ["Laguna Negra"] = { latitude = -54.524730, longitude = -67.292677, display_name = ("Laguna Negra")}, ["Estancia la Oriental"] = { latitude = -47.804487, longitude = -72.094099, display_name = ("Estancia la Oriental")}, ["Nativos"] = { latitude = -47.743195, longitude = -65.910980, display_name = ("Nativos")}, ["Manantiales"] = { latitude = -50.341434, longitude = -72.298383, display_name = ("Manantiales")}, ["Nueva Soberana"] = { latitude = -50.315879, longitude = -72.314947, display_name = ("Nueva Soberana")}, ["Las Barrancas"] = { latitude = -53.756583, longitude = -67.729884, display_name = ("Las Barrancas")}, ["Cerro Calafate"] = { latitude = -50.345661, longitude = -72.260605, display_name = ("Cerro Calafate")}, ["Condor Cliff"] = { latitude = -50.220282, longitude = -70.931250, display_name = ("Condor Cliff")}, ["Terrazas de Manantiales"] = { latitude = -50.344153, longitude = -72.310335, display_name = ("Terrazas de Manantiales")}, ["Huiliche"] = { latitude = -50.364216, longitude = -72.261491, display_name = ("Huiliche")}, ["Las Buitreras"] = { latitude = -51.725306, longitude = -70.132894, display_name = ("Las Buitreras")}, ["Las Piedras"] = { latitude = -50.341583, longitude = -72.253668, display_name = ("Las Piedras")}, ["Salesiano"] = { latitude = -50.348902, longitude = -72.277424, display_name = ("Salesiano")}, ["Lago Argentino"] = { latitude = -50.344446, longitude = -72.276446, display_name = ("Lago Argentino")}, ["Linda Vista"] = { latitude = -50.353162, longitude = -72.289283, display_name = ("Linda Vista")}, ["Casco Viejo"] = { latitude = -50.334722, longitude = -72.266390, display_name = ("Casco Viejo")}, ["Plumas Verdes"] = { latitude = -50.341102, longitude = -72.284337, display_name = ("Plumas Verdes")}, ["Ototel Aike"] = { latitude = -50.818402, longitude = -69.515470, display_name = ("Ototel Aike")}, ["Severino Amelung"] = { latitude = -47.744353, longitude = -65.882563, display_name = ("Severino Amelung")}, ["Prefectura"] = { latitude = -47.752433, longitude = -65.909614, display_name = ("Prefectura")}, ["Fuerte San Carlos"] = { latitude = -47.753911, longitude = -65.900867, display_name = ("Fuerte San Carlos")}, ["Mirador"] = { latitude = -47.747263, longitude = -65.901249, display_name = ("Mirador")}, ["Randisi"] = { latitude = -47.741155, longitude = -65.889550, display_name = ("Randisi")}, ["Centenario"] = { latitude = -47.745680, longitude = -65.888739, display_name = ("Centenario")}, ["Almafuerte"] = { latitude = -47.739640, longitude = -65.888611, display_name = ("Almafuerte")}, ["Del Alto"] = { latitude = -47.743659, longitude = -65.891165, display_name = ("Del Alto")}, ["Pevep"] = { latitude = -47.746133, longitude = -65.884953, display_name = ("Pevep")}, ["Vieira"] = { latitude = -47.745152, longitude = -65.895467, display_name = ("Vieira")}, ["Puerto Nuevo"] = { latitude = -47.741332, longitude = -65.882291, display_name = ("Puerto Nuevo")}, ["Quinta Cadario"] = { latitude = -47.749571, longitude = -65.900253, display_name = ("Quinta Cadario")}, ["La Quinta"] = { latitude = -49.357605, longitude = -72.869925, display_name = ("La Quinta")}, ["Estancia La Florida"] = { latitude = -48.796935, longitude = -72.551855, display_name = ("Estancia La Florida")}, ["Tres Cerros"] = { latitude = -48.143225, longitude = -67.652099, display_name = ("Tres Cerros")}, ["Estancia Rio Ewan"] = { latitude = -54.233493, longitude = -67.216310, display_name = ("Estancia Rio Ewan")}, ["Estancia Tepi"] = { latitude = -54.250082, longitude = -67.156238, display_name = ("Estancia Tepi")}, ["La Quinta"] = { latitude = -47.466841, longitude = -67.341048, display_name = ("La Quinta")}, ["Estancia San Jose"] = { latitude = -49.415962, longitude = -72.789599, display_name = ("Estancia San Jose")}, ["Los Coihues"] = { latitude = -51.535730, longitude = -72.339195, display_name = ("Los Coihues")}, ["Las Lengas"] = { latitude = -51.537738, longitude = -72.343467, display_name = ("Las Lengas")}, ["Islas Malvinas"] = { latitude = -51.534273, longitude = -72.329881, display_name = ("Islas Malvinas")}, ["Santa Cruz"] = { latitude = -51.539346, longitude = -72.334636, display_name = ("Santa Cruz")}, ["Las Margaritas"] = { latitude = -51.540984, longitude = -72.337259, display_name = ("Las Margaritas")}, ["Don Bosco"] = { latitude = -51.542416, longitude = -72.340417, display_name = ("Don Bosco")}, ["Los Lupinos"] = { latitude = -51.542482, longitude = -72.342855, display_name = ("Los Lupinos")}, ["Hielos Continentales"] = { latitude = -51.541815, longitude = -72.346996, display_name = ("Hielos Continentales")}, ["Puerto Bajo las Sombras"] = { latitude = -50.474128, longitude = -72.994062, display_name = ("Puerto Bajo las Sombras")}, ["Puerto Parry"] = { latitude = -54.798059, longitude = -64.365642, display_name = ("Puerto Parry")}, ["Cabo de San Juan"] = { latitude = -54.723422, longitude = -63.813242, display_name = ("Cabo de San Juan")}, ["Punta Cuchillo"] = { latitude = -54.836439, longitude = -64.753253, display_name = ("Punta Cuchillo")}, ["Faro Negro"] = { latitude = -49.257222, longitude = -67.643271, display_name = ("Faro Negro")}, ["Punta Guijarro"] = { latitude = -49.256868, longitude = -67.667435, display_name = ("Punta Guijarro")}, ["Santa Flavia"] = { latitude = -51.545274, longitude = -72.353929, display_name = ("Santa Flavia")}, ["Don Bosco"] = { latitude = -51.587917, longitude = -72.216739, display_name = ("Don Bosco")}, ["Frontera Austral"] = { latitude = -51.586442, longitude = -72.212121, display_name = ("Frontera Austral")}, ["Norte"] = { latitude = -51.576708, longitude = -72.212228, display_name = ("Norte")}, ["Quiroga"] = { latitude = -51.583425, longitude = -72.212723, display_name = ("Quiroga")}, ["Santa Cruz"] = { latitude = -51.581889, longitude = -72.215229, display_name = ("Santa Cruz")}, ["Julia Dufour"] = { latitude = -51.547098, longitude = -72.238197, display_name = ("Julia Dufour")}, ["Estancia Monte Entrance"] = { latitude = -50.070337, longitude = -68.515416, display_name = ("Estancia Monte Entrance")}, ["Tolhuin"] = { latitude = -54.511349, longitude = -67.195401, display_name = ("Tolhuin")}, ["Estancia Berta"] = { latitude = -52.009169, longitude = -72.038526, display_name = ("Estancia Berta")}, ["Estancia El Palenque"] = { latitude = -51.728046, longitude = -72.267951, display_name = ("Estancia El Palenque")}, ["Santa Margarita"] = { latitude = -49.558267, longitude = -72.425378, display_name = ("Santa Margarita")}, ["Estancia La Silesia"] = { latitude = -49.743146, longitude = -71.941012, display_name = ("Estancia La Silesia")}, ["Estancia Avellaneda"] = { latitude = -50.265333, longitude = -72.837388, display_name = ("Estancia Avellaneda")}, ["Estancia Los Ventisqueros"] = { latitude = -50.416912, longitude = -72.745527, display_name = ("Estancia Los Ventisqueros")}, ["Estancia Nibepo Aike"] = { latitude = -50.564280, longitude = -72.863877, display_name = ("Estancia Nibepo Aike")}, ["Chimen Aike"] = { latitude = -51.706819, longitude = -69.282341, display_name = ("Chimen Aike")}, ["Palermo Aike"] = { latitude = -51.705803, longitude = -69.717042, display_name = ("Palermo Aike")}, ["Campo de Invierno"] = { latitude = -50.319583, longitude = -72.246984, display_name = ("Campo de Invierno")}, ["Alto Calafate"] = { latitude = -50.338510, longitude = -72.232535, display_name = ("Alto Calafate")}, ["La Beatriz"] = { latitude = -47.453055, longitude = -67.105937, display_name = ("La Beatriz")}, ["Mancha Blanca"] = { latitude = -47.455083, longitude = -66.880167, display_name = ("Mancha Blanca")}, ["Altos de Soberana"] = { latitude = -50.320239, longitude = -72.325721, display_name = ("Altos de Soberana")}, ["Solo Terra"] = { latitude = -50.350922, longitude = -72.245328, display_name = ("Solo Terra")}, ["Felix Frias"] = { latitude = -50.333138, longitude = -72.257843, display_name = ("Felix Frias")}, ["Nuevo Manantiales"] = { latitude = -50.335789, longitude = -72.317838, display_name = ("Nuevo Manantiales")}, ["La Calandria"] = { latitude = -47.619570, longitude = -67.353367, display_name = ("La Calandria")}, ["La Castora"] = { latitude = -47.584397, longitude = -67.136232, display_name = ("La Castora")}, ["La Constancia"] = { latitude = -47.544866, longitude = -65.786829, display_name = ("La Constancia")}, ["Los Medanos"] = { latitude = -47.623271, longitude = -65.817736, display_name = ("Los Medanos")}, ["Campo Marina"] = { latitude = -47.770016, longitude = -66.018920, display_name = ("Campo Marina")}, ["Cerro del Paso"] = { latitude = -47.824283, longitude = -66.140911, display_name = ("Cerro del Paso")}, ["Puerto Jenkins"] = { latitude = -47.764178, longitude = -65.901115, display_name = ("Puerto Jenkins")}, ["Santa Elena"] = { latitude = -47.794311, longitude = -65.911261, display_name = ("Santa Elena")}, ["Papa Francisco"] = { latitude = -49.307534, longitude = -67.756625, display_name = ("Papa Francisco")}, ["Estancia La Siberia"] = { latitude = -49.014304, longitude = -71.061427, display_name = ("Estancia La Siberia")}, ["Chacras Piedrabuena"] = { latitude = -50.021569, longitude = -69.004401, display_name = ("Chacras Piedrabuena")}, ["Comercial"] = { latitude = -51.537104, longitude = -72.336783, display_name = ("Comercial")}, ["Los Mineros"] = { latitude = -51.537769, longitude = -72.332485, display_name = ("Los Mineros")}, ["Helena"] = { latitude = -49.897482, longitude = -68.607133, display_name = ("Helena")}, ["Los Granaderos"] = { latitude = -48.223989, longitude = -69.094696, display_name = ("Los Granaderos")}, ["Santa Catalina"] = { latitude = -48.297926, longitude = -69.076465, display_name = ("Santa Catalina")}, ["La Levadura"] = { latitude = -47.314061, longitude = -68.289946, display_name = ("La Levadura")}, ["Estancia Pirinaica"] = { latitude = -54.258577, longitude = -66.910229, display_name = ("Estancia Pirinaica")}, ["Estancia Puerto Rancho"] = { latitude = -55.046937, longitude = -66.561407, display_name = ("Estancia Puerto Rancho")}, ["Rancho Tres Amigos"] = { latitude = -54.632427, longitude = -65.280272, display_name = ("Rancho Tres Amigos")}, ["Terrazas de Nimez"] = { latitude = -50.322286, longitude = -72.260833, display_name = ("Terrazas de Nimez")}, ["La Pampita"] = { latitude = -49.306602, longitude = -67.774832, display_name = ("La Pampita")}, ["Arroyo El Sucio"] = { latitude = -48.191174, longitude = -72.309602, display_name = ("Arroyo El Sucio")}, ["Campamento Mayer"] = { latitude = -48.195712, longitude = -72.337717, display_name = ("Campamento Mayer")}, ["Entrada Mayer"] = { latitude = -48.209324, longitude = -72.323345, display_name = ("Entrada Mayer")}, ["Candelario Mancilla"] = { latitude = -48.868596, longitude = -72.743227, display_name = ("Candelario Mancilla")}, ["Estancia Ana"] = { latitude = -50.594091, longitude = -71.346879, display_name = ("Estancia Ana")}, ["Maipu"] = { latitude = -49.147049, longitude = -72.483949, display_name = ("Maipu")}, ["La Bernarda"] = { latitude = -49.162391, longitude = -71.926102, display_name = ("La Bernarda")}, ["Cerro Colorado"] = { latitude = -49.058749, longitude = -71.839008, display_name = ("Cerro Colorado")}, ["La Federica"] = { latitude = -49.036873, longitude = -72.238162, display_name = ("La Federica")}, ["La Tercera"] = { latitude = -49.186332, longitude = -72.364433, display_name = ("La Tercera")}, ["Sierra Nevada"] = { latitude = -48.922137, longitude = -72.265663, display_name = ("Sierra Nevada")}, ["San Adolfo"] = { latitude = -49.155453, longitude = -71.725175, display_name = ("San Adolfo")}, ["Editha"] = { latitude = -48.917580, longitude = -72.326083, display_name = ("Editha")}, ["La Lila"] = { latitude = -49.013507, longitude = -72.220039, display_name = ("La Lila")}, ["Chacabuco"] = { latitude = -49.055477, longitude = -72.313304, display_name = ("Chacabuco")}, ["Rio Meseta"] = { latitude = -49.282194, longitude = -71.921164, display_name = ("Rio Meseta")}, ["Lago Tar"] = { latitude = -49.277014, longitude = -72.002692, display_name = ("Lago Tar")}, ["Santa Angelita"] = { latitude = -49.207191, longitude = -72.224056, display_name = ("Santa Angelita")}, ["Hevia"] = { latitude = -48.912764, longitude = -72.411481, display_name = ("Hevia")}, ["La Montosa"] = { latitude = -47.299292, longitude = -72.027116, display_name = ("La Montosa")}, ["La Soledad"] = { latitude = -47.381977, longitude = -72.207754, display_name = ("La Soledad")}, ["Estancia La Suerte"] = { latitude = -49.138454, longitude = -72.937655, display_name = ("Estancia La Suerte")}, ["La gruta de Lurdes"] = { latitude = -47.687550, longitude = -66.014012, display_name = ("La gruta de Lurdes")}, ["Capilla"] = { latitude = -49.598686, longitude = -71.447858, display_name = ("Capilla")}, ["Las Vizcachas"] = { latitude = -50.762696, longitude = -72.048624, display_name = ("Las Vizcachas")}, ["Los Rubios"] = { latitude = -50.786879, longitude = -71.430251, display_name = ("Los Rubios")}, ["Los Viejos"] = { latitude = -50.794872, longitude = -71.347036, display_name = ("Los Viejos")}, ["Coronel Guarumba"] = { latitude = -50.862275, longitude = -71.815526, display_name = ("Coronel Guarumba")}, ["La Correntina"] = { latitude = -50.873398, longitude = -71.511372, display_name = ("La Correntina")}, ["Chorrillo"] = { latitude = -50.849175, longitude = -71.211925, display_name = ("Chorrillo")}, ["San Miguel"] = { latitude = -50.779461, longitude = -71.194408, display_name = ("San Miguel")}, ["Librun"] = { latitude = -50.840033, longitude = -71.099003, display_name = ("Librun")}, ["Rio Pelque"] = { latitude = -50.981447, longitude = -70.992185, display_name = ("Rio Pelque")}, ["Rivadavia"] = { latitude = -50.758697, longitude = -70.947480, display_name = ("Rivadavia")}, ["Boleadoras"] = { latitude = -50.867863, longitude = -70.810220, display_name = ("Boleadoras")}, ["La Chacra"] = { latitude = -50.982005, longitude = -70.676252, display_name = ("La Chacra")}, ["Peter"] = { latitude = -50.914322, longitude = -70.635606, display_name = ("Peter")}, ["La Carmen"] = { latitude = -50.948439, longitude = -70.292328, display_name = ("La Carmen")}, ["Monte Negro"] = { latitude = -51.041090, longitude = -69.869563, display_name = ("Monte Negro")}, ["Flora"] = { latitude = -50.968965, longitude = -70.361627, display_name = ("Flora")}, ["San Lorenzo"] = { latitude = -50.911558, longitude = -70.056938, display_name = ("San Lorenzo")}, ["Tres Lagunas"] = { latitude = -50.775976, longitude = -70.077831, display_name = ("Tres Lagunas")}, ["Dos Lagunas"] = { latitude = -50.920238, longitude = -69.858427, display_name = ("Dos Lagunas")}, ["Laguna Cifre"] = { latitude = -50.950353, longitude = -69.983062, display_name = ("Laguna Cifre")}, ["San Lorenzo"] = { latitude = -51.039574, longitude = -70.386488, display_name = ("San Lorenzo")}, ["Domi Aike"] = { latitude = -50.830417, longitude = -69.689989, display_name = ("Domi Aike")}, ["La Filomena"] = { latitude = -51.014731, longitude = -70.277271, display_name = ("La Filomena")}, ["Coy Inlet"] = { latitude = -50.927556, longitude = -69.218341, display_name = ("Coy Inlet")}, ["Los Alamos"] = { latitude = -50.913460, longitude = -69.398775, display_name = ("Los Alamos")}, ["La Aguada"] = { latitude = -50.945091, longitude = -69.340512, display_name = ("La Aguada")}, ["Moy"] = { latitude = -51.179483, longitude = -69.614896, display_name = ("Moy")}, ["Monte Carlo"] = { latitude = -51.024590, longitude = -70.629508, display_name = ("Monte Carlo")}, ["Maria Ines"] = { latitude = -51.032910, longitude = -70.574696, display_name = ("Maria Ines")}, ["Bandera"] = { latitude = -51.061142, longitude = -70.411205, display_name = ("Bandera")}, ["Camusu Aike"] = { latitude = -51.116296, longitude = -70.289348, display_name = ("Camusu Aike")}, ["La Carolina"] = { latitude = -51.059781, longitude = -70.171849, display_name = ("La Carolina")}, ["La Aida"] = { latitude = -51.167254, longitude = -70.014336, display_name = ("La Aida")}, ["Bandera"] = { latitude = -51.208097, longitude = -71.176304, display_name = ("Bandera")}, ["Lujan"] = { latitude = -51.176050, longitude = -70.994412, display_name = ("Lujan")}, ["La Vanguardia"] = { latitude = -51.021755, longitude = -71.111929, display_name = ("La Vanguardia")}, ["Chali Aike"] = { latitude = -51.047507, longitude = -70.856328, display_name = ("Chali Aike")}, ["La Correntina"] = { latitude = -50.916124, longitude = -71.764777, display_name = ("La Correntina")}, ["La Paloma"] = { latitude = -50.937877, longitude = -71.499452, display_name = ("La Paloma")}, ["La Felicidad"] = { latitude = -50.921110, longitude = -71.446839, display_name = ("La Felicidad")}, ["Ocho Hermanos"] = { latitude = -51.082876, longitude = -71.339920, display_name = ("Ocho Hermanos")}, ["La Nelida"] = { latitude = -51.057675, longitude = -71.284264, display_name = ("La Nelida")}, ["La Maria"] = { latitude = -51.052124, longitude = -71.603784, display_name = ("La Maria")}, ["Fuentes del Coyle"] = { latitude = -51.146514, longitude = -71.915765, display_name = ("Fuentes del Coyle")}, ["La Sarita Argentina"] = { latitude = -51.145214, longitude = -71.841294, display_name = ("La Sarita Argentina")}, ["La Lucila"] = { latitude = -50.997406, longitude = -71.908714, display_name = ("La Lucila")}, ["La Siberia"] = { latitude = -51.249165, longitude = -71.429055, display_name = ("La Siberia")}, ["Alaska"] = { latitude = -51.185763, longitude = -71.336965, display_name = ("Alaska")}, ["La Tapera"] = { latitude = -51.135301, longitude = -71.467681, display_name = ("La Tapera")}, ["La Lolita"] = { latitude = -51.153055, longitude = -71.579754, display_name = ("La Lolita")}, ["La Primavera"] = { latitude = -51.447714, longitude = -72.228270, display_name = ("La Primavera")}, ["Tres Marias"] = { latitude = -51.318977, longitude = -72.176226, display_name = ("Tres Marias")}, ["Stag River"] = { latitude = -51.663660, longitude = -72.025840, display_name = ("Stag River")}, ["La Fermina"] = { latitude = -51.690785, longitude = -72.083675, display_name = ("La Fermina")}, ["Punta Alta"] = { latitude = -51.684708, longitude = -71.966222, display_name = ("Punta Alta")}, ["Laguna Larga"] = { latitude = -51.808214, longitude = -71.882625, display_name = ("Laguna Larga")}, ["Estancia Glencross"] = { latitude = -51.828291, longitude = -71.692448, display_name = ("Estancia Glencross")}, ["Rincon de Los Morros"] = { latitude = -51.920101, longitude = -71.497324, display_name = ("Rincon de Los Morros")}, ["Cuadrado"] = { latitude = -51.743128, longitude = -71.315743, display_name = ("Cuadrado")}, ["Diez Y Ocho"] = { latitude = -51.776086, longitude = -71.123926, display_name = ("Diez Y Ocho")}, ["Chank Aike"] = { latitude = -51.453020, longitude = -70.514579, display_name = ("Chank Aike")}, ["San Jose"] = { latitude = -51.281311, longitude = -70.812392, display_name = ("San Jose")}, ["Minik Aike"] = { latitude = -51.422814, longitude = -71.081302, display_name = ("Minik Aike")}, ["Corpie Aike"] = { latitude = -51.353084, longitude = -70.654609, display_name = ("Corpie Aike")}, ["Los Vascos"] = { latitude = -51.431070, longitude = -70.863302, display_name = ("Los Vascos")}, ["La Obligada"] = { latitude = -51.275958, longitude = -71.175486, display_name = ("La Obligada")}, ["La Vega"] = { latitude = -51.444866, longitude = -70.330878, display_name = ("La Vega")}, ["Las Horquetas"] = { latitude = -51.395724, longitude = -70.224121, display_name = ("Las Horquetas")}, ["Suarez"] = { latitude = -51.380322, longitude = -70.213085, display_name = ("Suarez")}, ["Las Vegas"] = { latitude = -51.325617, longitude = -70.310106, display_name = ("Las Vegas")}, ["Cerro Cuadrado"] = { latitude = -51.320499, longitude = -70.002893, display_name = ("Cerro Cuadrado")}, ["Guer Aike"] = { latitude = -51.452278, longitude = -70.005562, display_name = ("Guer Aike")}, ["La Matilde"] = { latitude = -51.294548, longitude = -69.971165, display_name = ("La Matilde")}, ["Moy Aike Chico"] = { latitude = -51.247602, longitude = -69.624339, display_name = ("Moy Aike Chico")}, ["Los Pozos"] = { latitude = -51.451639, longitude = -69.315894, display_name = ("Los Pozos")}, ["Del Frigorifico"] = { latitude = -51.614578, longitude = -69.350605, display_name = ("Del Frigorifico")}, ["Killik Aike Sur"] = { latitude = -51.683113, longitude = -69.368351, display_name = ("Killik Aike Sur")}, ["Killik Aike Sur"] = { latitude = -51.617580, longitude = -69.466862, display_name = ("Killik Aike Sur")}, ["Killik Aike Norte"] = { latitude = -51.571289, longitude = -69.445227, display_name = ("Killik Aike Norte")}, ["Oeste"] = { latitude = -51.517235, longitude = -69.615717, display_name = ("Oeste")}, ["La Angelina"] = { latitude = -51.436005, longitude = -69.119121, display_name = ("La Angelina")}, ["Cabo Buen Tiempo"] = { latitude = -51.564513, longitude = -69.070573, display_name = ("Cabo Buen Tiempo")}, ["Del Rio Chico"] = { latitude = -51.671139, longitude = -69.118381, display_name = ("Del Rio Chico")}, ["Veintidos"] = { latitude = -51.356330, longitude = -69.580161, display_name = ("Veintidos")}, ["San Cristobal"] = { latitude = -51.515125, longitude = -70.085139, display_name = ("San Cristobal")}, ["La Leona"] = { latitude = -51.506458, longitude = -69.779421, display_name = ("La Leona")}, ["Mulato"] = { latitude = -51.924313, longitude = -70.879837, display_name = ("Mulato")}, ["Pampa"] = { latitude = -51.949091, longitude = -71.168665, display_name = ("Pampa")}, ["Dieciseis"] = { latitude = -51.854132, longitude = -71.253222, display_name = ("Dieciseis")}, ["La Carlota"] = { latitude = -51.847674, longitude = -70.516124, display_name = ("La Carlota")}, ["Cerro Negro"] = { latitude = -51.931893, longitude = -70.242680, display_name = ("Cerro Negro")}, ["La Maragata"] = { latitude = -51.868931, longitude = -70.219060, display_name = ("La Maragata")}, ["Alquinta"] = { latitude = -51.734466, longitude = -70.339546, display_name = ("Alquinta")}, ["Noya"] = { latitude = -51.722144, longitude = -69.987192, display_name = ("Noya")}, ["Laguna Colorada"] = { latitude = -51.667626, longitude = -69.917869, display_name = ("Laguna Colorada")}, ["Media Agua"] = { latitude = -51.840896, longitude = -69.881413, display_name = ("Media Agua")}, ["Bitzch"] = { latitude = -51.813905, longitude = -69.775300, display_name = ("Bitzch")}, ["La Argentina"] = { latitude = -51.899693, longitude = -70.042259, display_name = ("La Argentina")}, ["Pali Aike"] = { latitude = -51.983025, longitude = -69.748869, display_name = ("Pali Aike")}, ["Osasuna"] = { latitude = -51.955927, longitude = -69.891237, display_name = ("Osasuna")}, ["Markatch Aike"] = { latitude = -51.919502, longitude = -69.630911, display_name = ("Markatch Aike")}, ["Tres Hermanos"] = { latitude = -51.940785, longitude = -69.604545, display_name = ("Tres Hermanos")}, ["Cerro Norte"] = { latitude = -51.840693, longitude = -69.404454, display_name = ("Cerro Norte")}, ["Viejo"] = { latitude = -51.834961, longitude = -69.519280, display_name = ("Viejo")}, ["Frailes de Condor"] = { latitude = -51.913756, longitude = -69.122404, display_name = ("Frailes de Condor")}, ["Frailes de Loyola"] = { latitude = -51.870196, longitude = -68.972497, display_name = ("Frailes de Loyola")}, ["Negro"] = { latitude = -51.742656, longitude = -68.954570, display_name = ("Negro")}, ["Beta"] = { latitude = -52.726250, longitude = -68.547351, display_name = ("Beta")}, ["De La Costa"] = { latitude = -53.597802, longitude = -67.987795, display_name = ("De La Costa")}, ["Maria Behety"] = { latitude = -53.786150, longitude = -67.936297, display_name = ("Maria Behety")}, ["Aurelia"] = { latitude = -53.899264, longitude = -68.368782, display_name = ("Aurelia")}, ["San Luis"] = { latitude = -53.932653, longitude = -67.613643, display_name = ("San Luis")}, ["Entre Rios"] = { latitude = -54.130726, longitude = -67.272466, display_name = ("Entre Rios")}, ["Buenos Aires"] = { latitude = -54.162247, longitude = -67.647718, display_name = ("Buenos Aires")}, ["La Ruby"] = { latitude = -54.156781, longitude = -67.726511, display_name = ("La Ruby")}, ["Dos Hermanas"] = { latitude = -54.106919, longitude = -67.859184, display_name = ("Dos Hermanas")}, ["Herminita"] = { latitude = -54.024204, longitude = -67.989174, display_name = ("Herminita")}, ["La Constancia"] = { latitude = -54.028761, longitude = -68.294282, display_name = ("La Constancia")}, ["La Libertad"] = { latitude = -54.096262, longitude = -68.287415, display_name = ("La Libertad")}, ["Maria Laura"] = { latitude = -54.156995, longitude = -68.308915, display_name = ("Maria Laura")}, ["Rosita"] = { latitude = -54.021954, longitude = -68.472809, display_name = ("Rosita")}, ["Sierra Nevada"] = { latitude = -54.277743, longitude = -68.380788, display_name = ("Sierra Nevada")}, ["Astrid"] = { latitude = -54.271704, longitude = -68.002457, display_name = ("Astrid")}, ["Miramonte"] = { latitude = -54.210512, longitude = -67.744277, display_name = ("Miramonte")}, ["La Criolla"] = { latitude = -54.214327, longitude = -67.510475, display_name = ("La Criolla")}, ["Catalana"] = { latitude = -54.173351, longitude = -67.278732, display_name = ("Catalana")}, ["Las Hijas"] = { latitude = -54.211277, longitude = -67.278314, display_name = ("Las Hijas")}, ["La Ines"] = { latitude = -54.176529, longitude = -67.030391, display_name = ("La Ines")}, ["Los Cerros"] = { latitude = -54.335686, longitude = -67.856079, display_name = ("Los Cerros")}, ["La Rinconada"] = { latitude = -54.371214, longitude = -67.804838, display_name = ("La Rinconada")}, ["Parador Yawen"] = { latitude = -54.368807, longitude = -67.716894, display_name = ("Parador Yawen")}, ["Rivadavia"] = { latitude = -54.355029, longitude = -67.540597, display_name = ("Rivadavia")}, ["Indiana"] = { latitude = -54.347025, longitude = -67.422838, display_name = ("Indiana")}, ["Aserradero Americano"] = { latitude = -54.465721, longitude = -67.460604, display_name = ("Aserradero Americano")}, ["La Laguna"] = { latitude = -54.423523, longitude = -67.553129, display_name = ("La Laguna")}, ["Tonite"] = { latitude = -54.316052, longitude = -67.272485, display_name = ("Tonite")}, ["Santa Ana"] = { latitude = -54.287321, longitude = -66.756918, display_name = ("Santa Ana")}, ["Estancia Pirinaica"] = { latitude = -54.258577, longitude = -66.910229, display_name = ("Estancia Pirinaica")}, ["Estancia Tepi"] = { latitude = -54.250082, longitude = -67.156238, display_name = ("Estancia Tepi")}, ["Pilarica"] = { latitude = -54.114219, longitude = -68.189271, display_name = ("Pilarica")}, ["Las Buitreras"] = { latitude = -51.725220, longitude = -70.128850, display_name = ("Las Buitreras")}, ["Gobernador Mayer"] = { latitude = -51.348268, longitude = -70.291809, display_name = ("Gobernador Mayer")}, ["Puente Blanco"] = { latitude = -51.892305, longitude = -71.594124, display_name = ("Puente Blanco")}, ["La Leona"] = { latitude = -49.809259, longitude = -72.053103, display_name = ("La Leona")}, ["La Esperanza"] = { latitude = -51.022092, longitude = -70.885743, display_name = ("La Esperanza")}, ["La Barrancosa"] = { latitude = -50.199745, longitude = -70.213738, display_name = ("La Barrancosa")}, ["Estancia San Ramon"] = { latitude = -50.196600, longitude = -69.906312, display_name = ("Estancia San Ramon")}, ["Rincon"] = { latitude = -50.294309, longitude = -69.918822, display_name = ("Rincon")}, ["La Peninsula"] = { latitude = -47.460896, longitude = -71.863000, display_name = ("La Peninsula")}, ["La Peninsula"] = { latitude = -49.064440, longitude = -68.315556, display_name = ("La Peninsula")}, ["Alma Gaucha"] = { latitude = -47.888418, longitude = -66.948698, display_name = ("Alma Gaucha")}, ["Las Chacras"] = { latitude = -48.478452, longitude = -72.557845, display_name = ("Las Chacras")}, ["Dorotea"] = { latitude = -51.647924, longitude = -72.339118, display_name = ("Dorotea")}, ["Huertos Dorotea"] = { latitude = -51.618157, longitude = -72.332252, display_name = ("Huertos Dorotea")}, ["Monte Aymond"] = { latitude = -52.146118, longitude = -69.516473, display_name = ("Monte Aymond")}, ["Paso Laurita"] = { latitude = -51.689279, longitude = -72.298001, display_name = ("Paso Laurita")}, ["Capilla"] = { latitude = -50.315016, longitude = -72.791469, display_name = ("Capilla")}, ["Playita de Pescadores"] = { latitude = -49.216693, longitude = -72.984858, display_name = ("Playita de Pescadores")}, ["Piedra del Fraile"] = { latitude = -49.227558, longitude = -73.013723, display_name = ("Piedra del Fraile")}, ["Convivir"] = { latitude = -47.739251, longitude = -65.902745, display_name = ("Convivir")}, ["Los Notros"] = { latitude = -50.328950, longitude = -72.242808, display_name = ("Los Notros")}, ["Comandante Luis Piedrabuena"] = { latitude = -49.987276, longitude = -68.912991, display_name = ("Comandante Luis Piedrabuena")}, ["Brazo Norte"] = { latitude = -52.056235, longitude = -70.005431, display_name = ("Brazo Norte")}, ["Costa Centro"] = { latitude = -54.808705, longitude = -68.304695, display_name = ("Costa Centro")}, ["Grota Antonio Gil"] = { latitude = -51.500582, longitude = -72.252174, display_name = ("Grota Antonio Gil")}, ["Estancia Don Braulio"] = { latitude = -51.718385, longitude = -69.296456, display_name = ("Estancia Don Braulio")}, ["Estancia Entre Rios"] = { latitude = -48.265328, longitude = -72.218942, display_name = ("Estancia Entre Rios")}, ["Puerto Almanza"] = { latitude = -54.870409, longitude = -67.564406, display_name = ("Puerto Almanza")}, ["Estancia Moat"] = { latitude = -54.973617, longitude = -66.745034, display_name = ("Estancia Moat")}, ["Dungeness"] = { latitude = -52.394832, longitude = -68.431165, display_name = ("Dungeness")}, ["Interior Dungeness"] = { latitude = -52.339613, longitude = -68.469438, display_name = ("Interior Dungeness")}, ["Las Barrancas"] = { latitude = -52.318940, longitude = -68.587263, display_name = ("Las Barrancas")}, ["Daniel Uno"] = { latitude = -52.342340, longitude = -68.666549, display_name = ("Daniel Uno")}, ["Puesto Alamiro"] = { latitude = -52.228077, longitude = -69.023152, display_name = ("Puesto Alamiro")}, ["Puesto Chocolate"] = { latitude = -52.176616, longitude = -69.453188, display_name = ("Puesto Chocolate")}, ["Puesto Mulato"] = { latitude = -52.018599, longitude = -71.252006, display_name = ("Puesto Mulato")}, ["Puesto Pioneros"] = { latitude = -52.026759, longitude = -71.540790, display_name = ("Puesto Pioneros")}, ["Provincias Unidas"] = { latitude = -54.495274, longitude = -67.208691, display_name = ("Provincias Unidas")}, ["Islas del Sur"] = { latitude = -54.518772, longitude = -67.201981, display_name = ("Islas del Sur")}, ["Los Naranjos"] = { latitude = -54.513628, longitude = -67.207170, display_name = ("Los Naranjos")}, ["Las Violetas"] = { latitude = -53.672897, longitude = -67.926644, display_name = ("Las Violetas")}, ["Monte Aymond"] = { latitude = -52.102844, longitude = -69.533337, display_name = ("Monte Aymond")}, ["Monte Dinero"] = { latitude = -52.323463, longitude = -68.559501, display_name = ("Monte Dinero")}, ["Paso Rodolfo Roballos"] = { latitude = -47.133332, longitude = -71.850023, display_name = ("Paso Rodolfo Roballos")}, ["Estancia Harberton"] = { latitude = -54.877762, longitude = -67.329386, display_name = ("Estancia Harberton")}, ["Puerto Remolino"] = { latitude = -54.860454, longitude = -67.859145, display_name = ("Puerto Remolino")}, ["Estancia San Justo"] = { latitude = -54.028494, longitude = -68.544922, display_name = ("Estancia San Justo")}, ["Veintiocho de Noviembre"] = { latitude = -51.579567, longitude = -72.211488, display_name = ("Veintiocho de Noviembre")}, ["Cancha Carrera"] = { latitude = -51.256817, longitude = -72.220102, display_name = ("Cancha Carrera")}, ["Castillo"] = { latitude = -53.668110, longitude = -68.465338, display_name = ("Castillo")}, ["Diego Ritchie"] = { latitude = -51.978050, longitude = -70.396159, display_name = ("Diego Ritchie")}, ["Rospentek"] = { latitude = -51.665372, longitude = -72.145268, display_name = ("Rospentek")}, ["La Marina"] = { latitude = -54.316667, longitude = -68.550002, display_name = ("La Marina")}, ["Lapataia"] = { latitude = -54.855622, longitude = -68.577236, display_name = ("Lapataia")}, ["Puerto Curtze"] = { latitude = -52.802679, longitude = -71.394580, display_name = ("Puerto Curtze")}, ["Lazo"] = { latitude = -51.123177, longitude = -72.821959, display_name = ("Lazo")}, ["Monte Alto"] = { latitude = -52.533167, longitude = -70.966644, display_name = ("Monte Alto")}, ["Aserradero La Paciencia"] = { latitude = -54.369495, longitude = -69.162109, display_name = ("Aserradero La Paciencia")}, ["Caleta Clarencia"] = { latitude = -52.892751, longitude = -70.106510, display_name = ("Caleta Clarencia")}, ["La Quema"] = { latitude = -47.599999, longitude = -73.100039, display_name = ("La Quema")}, ["Baquedano"] = { latitude = -53.266667, longitude = -69.983337, display_name = ("Baquedano")}, ["Animales"] = { latitude = -47.833332, longitude = -73.466710, display_name = ("Animales")}, ["San Juan"] = { latitude = -53.644662, longitude = -70.958192, display_name = ("San Juan")}, ["Punta Espora"] = { latitude = -52.496010, longitude = -69.520669, display_name = ("Punta Espora")}, ["Caleta Ferrari"] = { latitude = -54.857108, longitude = -68.819531, display_name = ("Caleta Ferrari")}, ["Dumestre"] = { latitude = -51.788672, longitude = -72.467155, display_name = ("Dumestre")}, ["Ekewern"] = { latitude = -52.866667, longitude = -69.516670, display_name = ("Ekewern")}, ["Caleta Tortel"] = { latitude = -47.803471, longitude = -73.537689, display_name = ("Caleta Tortel")}, ["Seguel"] = { latitude = -46.949489, longitude = -72.793955, display_name = ("Seguel")}, ["Cochrane"] = { latitude = -47.254164, longitude = -72.573244, display_name = ("Cochrane")}, ["Puerto Nuevo"] = { latitude = -53.348604, longitude = -69.436163, display_name = ("Puerto Nuevo")}, ["Puerto Prat"] = { latitude = -51.636103, longitude = -72.648449, display_name = ("Puerto Prat")}, ["Tres Puentes"] = { latitude = -53.119153, longitude = -70.875206, display_name = ("Tres Puentes")}, ["Puerto Sara"] = { latitude = -52.623815, longitude = -70.199421, display_name = ("Puerto Sara")}, ["Bombalot"] = { latitude = -52.269362, longitude = -71.185096, display_name = ("Bombalot")}, ["Manantiales"] = { latitude = -52.545090, longitude = -69.420827, display_name = ("Manantiales")}, ["Puerto Progreso"] = { latitude = -52.479026, longitude = -69.469991, display_name = ("Puerto Progreso")}, ["Puerto Guadal"] = { latitude = -46.844571, longitude = -72.704322, display_name = ("Puerto Guadal")}, ["Porvenir"] = { latitude = -53.295674, longitude = -70.368745, display_name = ("Porvenir")}, ["Puerto Williams"] = { latitude = -54.935496, longitude = -67.606704, display_name = ("Puerto Williams")}, ["Puerto Zenteno"] = { latitude = -52.785992, longitude = -70.804605, display_name = ("Puerto Zenteno")}, ["Bersovio el Covacio"] = { latitude = -53.433333, longitude = -70.066671, display_name = ("Bersovio el Covacio")}, ["Carpa"] = { latitude = -52.598697, longitude = -71.201065, display_name = ("Carpa")}, ["Puerto Bertrand"] = { latitude = -47.015128, longitude = -72.827529, display_name = ("Puerto Bertrand")}, ["Reserva Fiscal"] = { latitude = -52.650000, longitude = -69.350003, display_name = ("Reserva Fiscal")}, ["Puerto Navarino"] = { latitude = -54.925972, longitude = -68.323515, display_name = ("Puerto Navarino")}, ["Puerto Yartou"] = { latitude = -53.885965, longitude = -70.140251, display_name = ("Puerto Yartou")}, ["Caleta Puerto Nuevo"] = { latitude = -47.676575, longitude = -73.081475, display_name = ("Caleta Puerto Nuevo")}, ["Punta Catalina"] = { latitude = -52.548403, longitude = -68.757271, display_name = ("Punta Catalina")}, ["Isla Dawson"] = { latitude = -53.890515, longitude = -70.685742, display_name = ("Isla Dawson")}, ["Reserva Nacional Tamango"] = { latitude = -47.242465, longitude = -72.524046, display_name = ("Reserva Nacional Tamango")}, ["Punta Mancilla"] = { latitude = -47.801782, longitude = -73.534946, display_name = ("Punta Mancilla")}, ["Punta Rodolfo"] = { latitude = -47.806386, longitude = -73.548738, display_name = ("Punta Rodolfo")}, ["Cerro Sombrero"] = { latitude = -52.776462, longitude = -69.290911, display_name = ("Cerro Sombrero")}, ["Caleta Piedras"] = { latitude = -55.036483, longitude = -67.021939, display_name = ("Caleta Piedras")}, ["Caleta Banner"] = { latitude = -55.019887, longitude = -66.925857, display_name = ("Caleta Banner")}, ["Caleta Eugenia"] = { latitude = -54.932967, longitude = -67.312696, display_name = ("Caleta Eugenia")}, ["Caleta Las Casas"] = { latitude = -55.270341, longitude = -66.643108, display_name = ("Caleta Las Casas")}, ["Caleta Carlos"] = { latitude = -55.177094, longitude = -66.548154, display_name = ("Caleta Carlos")}, ["Kemoa"] = { latitude = -55.187355, longitude = -67.457751, display_name = ("Kemoa")}, ["Caleta Wulaia"] = { latitude = -55.047595, longitude = -68.147134, display_name = ("Caleta Wulaia")}, ["Estancia Puerto Consuelo"] = { latitude = -51.604313, longitude = -72.656923, display_name = ("Estancia Puerto Consuelo")}, ["Cerro Castillo"] = { latitude = -51.256249, longitude = -72.344706, display_name = ("Cerro Castillo")}, ["Estancia Tranquilo"] = { latitude = -51.822437, longitude = -72.164226, display_name = ("Estancia Tranquilo")}, ["Puerto Toro"] = { latitude = -51.412983, longitude = -73.079812, display_name = ("Puerto Toro")}, ["Mina Guarello"] = { latitude = -50.367541, longitude = -75.337388, display_name = ("Mina Guarello")}, ["Barrio Industrial"] = { latitude = -53.106547, longitude = -70.890182, display_name = ("Barrio Industrial")}, ["Laguna Lynch"] = { latitude = -53.173128, longitude = -71.001413, display_name = ("Laguna Lynch")}, ["Barranco Amarrillo"] = { latitude = -53.095566, longitude = -70.878004, display_name = ("Barranco Amarrillo")}, ["Chabunco"] = { latitude = -52.988120, longitude = -70.814839, display_name = ("Chabunco")}, ["Kon Aiken"] = { latitude = -52.947987, longitude = -70.844535, display_name = ("Kon Aiken")}, ["Cabo Negro"] = { latitude = -52.974696, longitude = -70.825217, display_name = ("Cabo Negro")}, ["Los Calafates"] = { latitude = -53.034450, longitude = -70.861454, display_name = ("Los Calafates")}, ["Estancia Yendegaia"] = { latitude = -54.858305, longitude = -68.820743, display_name = ("Estancia Yendegaia")}, ["Islas Ildefonso"] = { latitude = -55.838351, longitude = -69.348071, display_name = ("Islas Ildefonso")}, ["Estancia Almirantazgo"] = { latitude = -54.461441, longitude = -69.087070, display_name = ("Estancia Almirantazgo")}, ["Estancia Las Flores"] = { latitude = -53.685009, longitude = -68.702034, display_name = ("Estancia Las Flores")}, ["Estancia El Estero"] = { latitude = -53.605440, longitude = -68.745337, display_name = ("Estancia El Estero")}, ["Estancia La Laguna"] = { latitude = -53.470639, longitude = -68.724388, display_name = ("Estancia La Laguna")}, ["Puerto Bories"] = { latitude = -51.689948, longitude = -72.536391, display_name = ("Puerto Bories")}, ["Estancia Valdivia"] = { latitude = -51.503187, longitude = -72.688892, display_name = ("Estancia Valdivia")}, ["Estancia Entre Vientos"] = { latitude = -52.778965, longitude = -71.278431, display_name = ("Estancia Entre Vientos")}, ["Gobernador Philippi"] = { latitude = -52.747488, longitude = -71.009229, display_name = ("Gobernador Philippi")}, ["Puerto Harris"] = { latitude = -53.835225, longitude = -70.453125, display_name = ("Puerto Harris")}, ["Parroquia"] = { latitude = -47.253452, longitude = -72.581531, display_name = ("Parroquia")}, ["Capilla Puerto Bertrand"] = { latitude = -47.015288, longitude = -72.828226, display_name = ("Capilla Puerto Bertrand")}, ["Parroquia"] = { latitude = -53.301643, longitude = -70.437442, display_name = ("Parroquia")}, ["Estancia Draga"] = { latitude = -53.415109, longitude = -70.052471, display_name = ("Estancia Draga")}, ["Los Canelos"] = { latitude = -53.464275, longitude = -70.179761, display_name = ("Los Canelos")}, ["Estancia Armonia"] = { latitude = -53.335002, longitude = -69.774712, display_name = ("Estancia Armonia")}, ["Estancia Jeanette Naria"] = { latitude = -53.314245, longitude = -69.731072, display_name = ("Estancia Jeanette Naria")}, ["Estancia Maria Soledad"] = { latitude = -53.325603, longitude = -69.675789, display_name = ("Estancia Maria Soledad")}, ["Estancia Zenia"] = { latitude = -53.318641, longitude = -69.701763, display_name = ("Estancia Zenia")}, ["Estancia Los Copihues"] = { latitude = -53.336877, longitude = -69.456837, display_name = ("Estancia Los Copihues")}, ["Caleta Los Crillos"] = { latitude = -53.339076, longitude = -69.608123, display_name = ("Caleta Los Crillos")}, ["Estancia Teresita"] = { latitude = -53.330793, longitude = -69.761221, display_name = ("Estancia Teresita")}, ["Onaisin"] = { latitude = -53.387012, longitude = -69.280914, display_name = ("Onaisin")}, ["La Rinconada"] = { latitude = -46.757430, longitude = -72.898063, display_name = ("La Rinconada")}, ["Miraflores"] = { latitude = -46.755841, longitude = -72.872506, display_name = ("Miraflores")}, ["Ocho Hermanas"] = { latitude = -47.120720, longitude = -72.881843, display_name = ("Ocho Hermanas")}, ["Puesto Tejuela"] = { latitude = -47.176759, longitude = -72.366164, display_name = ("Puesto Tejuela")}, ["Potrero Las Vacas"] = { latitude = -47.049537, longitude = -72.383845, display_name = ("Potrero Las Vacas")}, ["Los Carreras"] = { latitude = -47.040647, longitude = -72.347796, display_name = ("Los Carreras")}, ["Ripidos del Baker"] = { latitude = -47.042200, longitude = -72.818297, display_name = ("Ripidos del Baker")}, ["Los Tres Limones"] = { latitude = -47.303094, longitude = -72.970077, display_name = ("Los Tres Limones")}, ["Los Leones"] = { latitude = -47.213851, longitude = -72.869481, display_name = ("Los Leones")}, ["Valle Chacabuco"] = { latitude = -47.117020, longitude = -72.486045, display_name = ("Valle Chacabuco")}, ["La Piedra Blanca"] = { latitude = -47.296636, longitude = -72.916975, display_name = ("La Piedra Blanca")}, ["Rio Nef"] = { latitude = -47.112893, longitude = -72.925101, display_name = ("Rio Nef")}, ["Valle Grande"] = { latitude = -47.281788, longitude = -72.727980, display_name = ("Valle Grande")}, ["Poco a Poco"] = { latitude = -47.333421, longitude = -72.755587, display_name = ("Poco a Poco")}, ["Calluqueo"] = { latitude = -47.657245, longitude = -72.477491, display_name = ("Calluqueo")}, ["La Rinconada"] = { latitude = -47.315269, longitude = -73.036430, display_name = ("La Rinconada")}, ["La Juanita"] = { latitude = -47.336737, longitude = -72.174784, display_name = ("La Juanita")}, ["Bajo Pascua"] = { latitude = -48.228838, longitude = -73.215855, display_name = ("Bajo Pascua")}, ["Caleta Puerto Alegre"] = { latitude = -47.747700, longitude = -73.241568, display_name = ("Caleta Puerto Alegre")}, ["La Esperanza"] = { latitude = -47.470873, longitude = -72.539033, display_name = ("La Esperanza")}, ["Laguna Larga"] = { latitude = -47.468441, longitude = -72.804897, display_name = ("Laguna Larga")}, ["Valle La Tranquera"] = { latitude = -47.607010, longitude = -72.930341, display_name = ("Valle La Tranquera")}, ["La Peligrosa"] = { latitude = -47.380286, longitude = -72.247977, display_name = ("La Peligrosa")}, ["La Delicia"] = { latitude = -47.478502, longitude = -72.482797, display_name = ("La Delicia")}, ["La Rinconada"] = { latitude = -47.566979, longitude = -72.523841, display_name = ("La Rinconada")}, ["Caleta Yungay"] = { latitude = -47.933368, longitude = -73.324544, display_name = ("Caleta Yungay")}, ["Caleta Cachorro"] = { latitude = -47.704420, longitude = -73.090578, display_name = ("Caleta Cachorro")}, ["Santa Margarita"] = { latitude = -47.332510, longitude = -72.847759, display_name = ("Santa Margarita")}, ["La Isla"] = { latitude = -47.332723, longitude = -72.870774, display_name = ("La Isla")}, ["Arroyo Chueco"] = { latitude = -47.337493, longitude = -72.962271, display_name = ("Arroyo Chueco")}, ["Lago Quetru"] = { latitude = -48.146783, longitude = -73.107067, display_name = ("Lago Quetru")}, ["Puesto Nutria"] = { latitude = -52.544696, longitude = -72.031135, display_name = ("Puesto Nutria")}, ["Cacique Mulato"] = { latitude = -52.456263, longitude = -71.338350, display_name = ("Cacique Mulato")}, ["Puerto Slight"] = { latitude = -46.805303, longitude = -75.582010, display_name = ("Puerto Slight")}, ["Pesquera Mac Lean"] = { latitude = -54.940111, longitude = -67.712389, display_name = ("Pesquera Mac Lean")}, ["Caleta Douglas"] = { latitude = -55.174065, longitude = -68.103992, display_name = ("Caleta Douglas")}, ["Estancia Dora"] = { latitude = -52.198713, longitude = -72.555850, display_name = ("Estancia Dora")}, ["Estancia Lago Pinto"] = { latitude = -52.060065, longitude = -72.399181, display_name = ("Estancia Lago Pinto")}, ["Estancia Puerto Aldea"] = { latitude = -52.138100, longitude = -72.654624, display_name = ("Estancia Puerto Aldea")}, ["Estancia Linacre"] = { latitude = -52.041183, longitude = -72.664249, display_name = ("Estancia Linacre")}, ["Estancia Solar"] = { latitude = -51.930372, longitude = -72.362387, display_name = ("Estancia Solar")}, ["Estancia Tres Pasos"] = { latitude = -51.420599, longitude = -72.466943, display_name = ("Estancia Tres Pasos")}, ["Estancia Florita"] = { latitude = -52.815156, longitude = -71.401451, display_name = ("Estancia Florita")}, ["Estancia Santa Rosa de Lima"] = { latitude = -52.981408, longitude = -71.840572, display_name = ("Estancia Santa Rosa de Lima")}, ["Ankel"] = { latitude = -52.906627, longitude = -71.021444, display_name = ("Ankel")}, ["Estancia Don Pancho"] = { latitude = -52.689704, longitude = -71.904227, display_name = ("Estancia Don Pancho")}, ["Estancia Entre Vientos"] = { latitude = -52.744968, longitude = -71.450824, display_name = ("Estancia Entre Vientos")}, ["Estancia Eva"] = { latitude = -52.694340, longitude = -71.678652, display_name = ("Estancia Eva")}, ["Caleta"] = { latitude = -53.056841, longitude = -71.283647, display_name = ("Caleta")}, ["Ponsomby"] = { latitude = -52.654276, longitude = -71.470338, display_name = ("Ponsomby")}, ["Estancia Chilenita"] = { latitude = -52.701817, longitude = -71.804749, display_name = ("Estancia Chilenita")}, ["Estancia Mina Rica"] = { latitude = -53.020308, longitude = -71.048847, display_name = ("Estancia Mina Rica")}, ["Estancia Gloria"] = { latitude = -52.651371, longitude = -71.489438, display_name = ("Estancia Gloria")}, ["Puesto Once"] = { latitude = -52.677206, longitude = -71.948967, display_name = ("Puesto Once")}, ["Estancia El Salto"] = { latitude = -52.690165, longitude = -71.637657, display_name = ("Estancia El Salto")}, ["Estancia Rocallosa"] = { latitude = -52.658995, longitude = -71.973670, display_name = ("Estancia Rocallosa")}, ["Estancia Iniverno"] = { latitude = -52.888426, longitude = -71.607353, display_name = ("Estancia Iniverno")}, ["Estancia San Carlos"] = { latitude = -52.693495, longitude = -71.591260, display_name = ("Estancia San Carlos")}, ["Puesto Garay"] = { latitude = -52.669103, longitude = -72.093180, display_name = ("Puesto Garay")}, ["Puerto Percy"] = { latitude = -52.900763, longitude = -70.267330, display_name = ("Puerto Percy")}, ["Estancia la Oriental"] = { latitude = -47.804487, longitude = -72.094099, display_name = ("Estancia la Oriental")}, ["Iglesia Puerto Williams"] = { latitude = -54.934527, longitude = -67.610928, display_name = ("Iglesia Puerto Williams")}, ["Iglesia Cerro Sombrero"] = { latitude = -52.776627, longitude = -69.288748, display_name = ("Iglesia Cerro Sombrero")}, ["Punta Passage"] = { latitude = -50.766254, longitude = -74.420393, display_name = ("Punta Passage")}, ["Puerto Gray"] = { latitude = -48.919049, longitude = -74.313453, display_name = ("Puerto Gray")}, ["Peninsula Baquedano"] = { latitude = -48.722342, longitude = -74.322657, display_name = ("Peninsula Baquedano")}, ["Caleta Lackawana"] = { latitude = -49.168349, longitude = -74.408650, display_name = ("Caleta Lackawana")}, ["Caleta Lucas"] = { latitude = -49.000589, longitude = -74.407219, display_name = ("Caleta Lucas")}, ["Caleta Hoskyn"] = { latitude = -48.943895, longitude = -74.414108, display_name = ("Caleta Hoskyn")}, ["Caleta Level"] = { latitude = -49.118040, longitude = -74.355904, display_name = ("Caleta Level")}, ["Peninsula Broome"] = { latitude = -49.372174, longitude = -74.414700, display_name = ("Peninsula Broome")}, ["Caleta Grau"] = { latitude = -49.340337, longitude = -74.410710, display_name = ("Caleta Grau")}, ["Peninsula Elliott"] = { latitude = -49.338714, longitude = -74.416246, display_name = ("Peninsula Elliott")}, ["Punta Porpoise"] = { latitude = -50.740802, longitude = -74.519732, display_name = ("Punta Porpoise")}, ["Caleta Rayo"] = { latitude = -50.758314, longitude = -74.537411, display_name = ("Caleta Rayo")}, ["Punta Cutter"] = { latitude = -50.711786, longitude = -74.517988, display_name = ("Punta Cutter")}, ["Puerto Micaela"] = { latitude = -49.511862, longitude = -74.271064, display_name = ("Puerto Micaela")}, ["Caleta Lucrecia"] = { latitude = -49.511862, longitude = -74.266193, display_name = ("Caleta Lucrecia")}, ["Agua Fresca Sur"] = { latitude = -53.514716, longitude = -70.951519, display_name = ("Agua Fresca Sur")}, ["Templo San Juan Bosco"] = { latitude = -51.736955, longitude = -72.494012, display_name = ("Templo San Juan Bosco")}, ["Capilla Buen Pastor"] = { latitude = -51.731365, longitude = -72.490678, display_name = ("Capilla Buen Pastor")}, ["Cerro Pancho"] = { latitude = -51.686884, longitude = -72.492078, display_name = ("Cerro Pancho")}, ["Endesa"] = { latitude = -51.731143, longitude = -72.486350, display_name = ("Endesa")}, ["Etnias Unidas"] = { latitude = -51.739156, longitude = -72.481760, display_name = ("Etnias Unidas")}, ["Nueva Esperanza"] = { latitude = -51.733536, longitude = -72.479856, display_name = ("Nueva Esperanza")}, ["Octavio Castro"] = { latitude = -51.720114, longitude = -72.494355, display_name = ("Octavio Castro")}, ["Natales Antiguo"] = { latitude = -51.723359, longitude = -72.491379, display_name = ("Natales Antiguo")}, ["Nueva Patagonia"] = { latitude = -51.732517, longitude = -72.505584, display_name = ("Nueva Patagonia")}, ["Chile Nuevo"] = { latitude = -51.735838, longitude = -72.490441, display_name = ("Chile Nuevo")}, ["Costanera"] = { latitude = -51.734219, longitude = -72.504367, display_name = ("Costanera")}, ["Morro Chico"] = { latitude = -52.055267, longitude = -71.428107, display_name = ("Morro Chico")}, ["Juan Pablo II"] = { latitude = -51.740819, longitude = -72.491847, display_name = ("Juan Pablo II")}, ["Eduardo Frei Montalva"] = { latitude = -51.741191, longitude = -72.486353, display_name = ("Eduardo Frei Montalva")}, ["Torres del Paine"] = { latitude = -51.738845, longitude = -72.484287, display_name = ("Torres del Paine")}, ["Estancia Rosario"] = { latitude = -51.734992, longitude = -72.527791, display_name = ("Estancia Rosario")}, ["Estancia Mercedes"] = { latitude = -51.760973, longitude = -72.837520, display_name = ("Estancia Mercedes")}, ["Iglesia Metodista Chile Nuevo"] = { latitude = -51.734920, longitude = -72.491168, display_name = ("Iglesia Metodista Chile Nuevo")}, ["Puesto Gallego Chico"] = { latitude = -52.024748, longitude = -70.735214, display_name = ("Puesto Gallego Chico")}, ["Punta Yamana"] = { latitude = -54.967033, longitude = -69.031027, display_name = ("Punta Yamana")}, ["Laguna Amarga"] = { latitude = -50.979515, longitude = -72.800459, display_name = ("Laguna Amarga")}, ["Caleta Dos de Mayo"] = { latitude = -54.866105, longitude = -68.691247, display_name = ("Caleta Dos de Mayo")}, ["Punta Flores"] = { latitude = -54.921092, longitude = -68.253785, display_name = ("Punta Flores")}, ["Caleta Fique"] = { latitude = -54.908449, longitude = -68.212819, display_name = ("Caleta Fique")}, ["Punta Bartlett"] = { latitude = -54.903021, longitude = -68.222947, display_name = ("Punta Bartlett")}, ["Punta Moya"] = { latitude = -54.918267, longitude = -68.233847, display_name = ("Punta Moya")}, ["Caleta Segura"] = { latitude = -54.916540, longitude = -68.212304, display_name = ("Caleta Segura")}, ["Puerto Corriente"] = { latitude = -54.989434, longitude = -68.365872, display_name = ("Puerto Corriente")}, ["Caleta Molinare"] = { latitude = -55.000482, longitude = -68.352353, display_name = ("Caleta Molinare")}, ["Caleta Domingo"] = { latitude = -55.039458, longitude = -68.317066, display_name = ("Caleta Domingo")}, ["Caleta Contreras"] = { latitude = -54.873520, longitude = -68.775852, display_name = ("Caleta Contreras")}, ["Caleta Tanswuani"] = { latitude = -55.072564, longitude = -68.327241, display_name = ("Caleta Tanswuani")}, ["Punta Tasbani"] = { latitude = -55.079857, longitude = -68.329511, display_name = ("Punta Tasbani")}, ["Puerto Grandi"] = { latitude = -55.211109, longitude = -67.921785, display_name = ("Puerto Grandi")}, ["Puerto Micalvito"] = { latitude = -55.195510, longitude = -67.867415, display_name = ("Puerto Micalvito")}, ["Estancia La Florida"] = { latitude = -48.796935, longitude = -72.551855, display_name = ("Estancia La Florida")}, ["Caleta Bevan"] = { latitude = -55.229353, longitude = -67.440832, display_name = ("Caleta Bevan")}, ["Caleta Castillo"] = { latitude = -55.230168, longitude = -68.255568, display_name = ("Caleta Castillo")}, ["Caleta Canacus"] = { latitude = -55.219714, longitude = -68.298607, display_name = ("Caleta Canacus")}, ["Punta Carpintero"] = { latitude = -55.221301, longitude = -68.210509, display_name = ("Punta Carpintero")}, ["Punta Morrison"] = { latitude = -53.836012, longitude = -70.436359, display_name = ("Punta Morrison")}, ["Punta Tesner"] = { latitude = -53.836106, longitude = -70.421532, display_name = ("Punta Tesner")}, ["Punta Bernabe"] = { latitude = -53.838423, longitude = -70.427540, display_name = ("Punta Bernabe")}, ["Punta Rafael"] = { latitude = -53.855256, longitude = -70.431900, display_name = ("Punta Rafael")}, ["Punta Arboleda"] = { latitude = -53.864722, longitude = -70.420742, display_name = ("Punta Arboleda")}, ["Estancia La Cumbre"] = { latitude = -50.734411, longitude = -72.409431, display_name = ("Estancia La Cumbre")}, ["Isla Victor"] = { latitude = -47.176348, longitude = -72.114299, display_name = ("Isla Victor")}, ["Iglesia Exumenica"] = { latitude = -52.427218, longitude = -71.415102, display_name = ("Iglesia Exumenica")}, ["Los Coihues"] = { latitude = -51.535730, longitude = -72.339195, display_name = ("Los Coihues")}, ["Las Lengas"] = { latitude = -51.537738, longitude = -72.343467, display_name = ("Las Lengas")}, ["Islas Malvinas"] = { latitude = -51.534273, longitude = -72.329881, display_name = ("Islas Malvinas")}, ["Santa Cruz"] = { latitude = -51.539346, longitude = -72.334636, display_name = ("Santa Cruz")}, ["Las Margaritas"] = { latitude = -51.540984, longitude = -72.337259, display_name = ("Las Margaritas")}, ["Don Bosco"] = { latitude = -51.542416, longitude = -72.340417, display_name = ("Don Bosco")}, ["Los Lupinos"] = { latitude = -51.542482, longitude = -72.342855, display_name = ("Los Lupinos")}, ["Hielos Continentales"] = { latitude = -51.541815, longitude = -72.346996, display_name = ("Hielos Continentales")}, ["Huertos Familiares"] = { latitude = -51.698345, longitude = -72.488467, display_name = ("Huertos Familiares")}, ["Santa Flavia"] = { latitude = -51.545274, longitude = -72.353929, display_name = ("Santa Flavia")}, ["Don Bosco"] = { latitude = -51.587917, longitude = -72.216739, display_name = ("Don Bosco")}, ["Frontera Austral"] = { latitude = -51.586442, longitude = -72.212121, display_name = ("Frontera Austral")}, ["Norte"] = { latitude = -51.576708, longitude = -72.212228, display_name = ("Norte")}, ["Quiroga"] = { latitude = -51.583425, longitude = -72.212723, display_name = ("Quiroga")}, ["Santa Cruz"] = { latitude = -51.581889, longitude = -72.215229, display_name = ("Santa Cruz")}, ["Julia Dufour"] = { latitude = -51.547098, longitude = -72.238197, display_name = ("Julia Dufour")}, ["Estancia Don Doimo"] = { latitude = -52.789670, longitude = -71.091457, display_name = ("Estancia Don Doimo")}, ["Estancia La Invernada"] = { latitude = -52.278024, longitude = -71.347983, display_name = ("Estancia La Invernada")}, ["Estancia Creek"] = { latitude = -52.027585, longitude = -71.835858, display_name = ("Estancia Creek")}, ["Estancia Berta"] = { latitude = -52.009169, longitude = -72.038526, display_name = ("Estancia Berta")}, ["Estancia Flores"] = { latitude = -51.962168, longitude = -72.145017, display_name = ("Estancia Flores")}, ["Estancia Don Marcelino"] = { latitude = -51.758165, longitude = -72.247360, display_name = ("Estancia Don Marcelino")}, ["Estancia El Palenque"] = { latitude = -51.728046, longitude = -72.267951, display_name = ("Estancia El Palenque")}, ["Estancia Aurelia Del Carmen"] = { latitude = -52.748023, longitude = -71.013050, display_name = ("Estancia Aurelia Del Carmen")}, ["Agroturismo Los Manantiales"] = { latitude = -51.216136, longitude = -72.455777, display_name = ("Agroturismo Los Manantiales")}, ["Shuka"] = { latitude = -51.738760, longitude = -72.479123, display_name = ("Shuka")}, ["Terranova"] = { latitude = -51.738501, longitude = -72.477245, display_name = ("Terranova")}, ["Estadio"] = { latitude = -51.726139, longitude = -72.484459, display_name = ("Estadio")}, ["Corvi"] = { latitude = -51.733180, longitude = -72.491331, display_name = ("Corvi")}, ["Puerto Toro"] = { latitude = -55.083060, longitude = -67.075865, display_name = ("Puerto Toro")}, ["Comercial"] = { latitude = -51.537104, longitude = -72.336783, display_name = ("Comercial")}, ["Los Mineros"] = { latitude = -51.537769, longitude = -72.332485, display_name = ("Los Mineros")}, ["Estancia Skyring"] = { latitude = -52.549343, longitude = -71.958305, display_name = ("Estancia Skyring")}, ["Cerro Guido"] = { latitude = -50.940219, longitude = -72.454272, display_name = ("Cerro Guido")}, ["Estancia Dynevor"] = { latitude = -52.532138, longitude = -72.327485, display_name = ("Estancia Dynevor")}, ["Caleta Jara"] = { latitude = -52.533409, longitude = -72.199463, display_name = ("Caleta Jara")}, ["Caleta Aihuaia"] = { latitude = -54.945317, longitude = -68.431300, display_name = ("Caleta Aihuaia")}, ["Caleta Letier"] = { latitude = -54.942406, longitude = -68.462551, display_name = ("Caleta Letier")}, ["Caleta Molinate"] = { latitude = -55.009354, longitude = -68.334574, display_name = ("Caleta Molinate")}, ["Caleta Yamana"] = { latitude = -54.965738, longitude = -69.028905, display_name = ("Caleta Yamana")}, ["Caleta Sonia"] = { latitude = -54.960376, longitude = -69.010499, display_name = ("Caleta Sonia")}, ["Caleta Olla"] = { latitude = -54.940564, longitude = -69.146661, display_name = ("Caleta Olla")}, ["Caleta Evening"] = { latitude = -54.959179, longitude = -69.479897, display_name = ("Caleta Evening")}, ["Caleta Morning"] = { latitude = -54.920202, longitude = -69.496822, display_name = ("Caleta Morning")}, ["Estancia Santa Rosa"] = { latitude = -54.903466, longitude = -68.157187, display_name = ("Estancia Santa Rosa")}, ["Caleta Mejillones"] = { latitude = -54.900733, longitude = -68.002317, display_name = ("Caleta Mejillones")}, ["Puerto Mejillones"] = { latitude = -54.904021, longitude = -67.989808, display_name = ("Puerto Mejillones")}, ["Puerto Hondo"] = { latitude = -55.079105, longitude = -70.032788, display_name = ("Puerto Hondo")}, ["Puerto Almeida"] = { latitude = -54.854675, longitude = -70.657038, display_name = ("Puerto Almeida")}, ["Puerto Ballenas"] = { latitude = -54.847283, longitude = -70.550351, display_name = ("Puerto Ballenas")}, ["Caleta Aguada"] = { latitude = -55.072852, longitude = -70.840915, display_name = ("Caleta Aguada")}, ["Isla Treble"] = { latitude = -55.092597, longitude = -71.035583, display_name = ("Isla Treble")}, ["Caleta Doris"] = { latitude = -54.979973, longitude = -71.139211, display_name = ("Caleta Doris")}, ["Isla Selma"] = { latitude = -54.921881, longitude = -71.475756, display_name = ("Isla Selma")}, ["Cabo Fletcher"] = { latitude = -54.653279, longitude = -71.308521, display_name = ("Cabo Fletcher")}, ["Isla Astorga"] = { latitude = -54.597906, longitude = -72.077724, display_name = ("Isla Astorga")}, ["Caleta Brecknock"] = { latitude = -54.551152, longitude = -71.877160, display_name = ("Caleta Brecknock")}, ["Puerto Consuelo"] = { latitude = -54.545932, longitude = -71.516158, display_name = ("Puerto Consuelo")}, ["Puerto Tanteo"] = { latitude = -54.413299, longitude = -71.499703, display_name = ("Puerto Tanteo")}, ["Puerto King"] = { latitude = -54.422819, longitude = -71.221705, display_name = ("Puerto King")}, ["Caleta Wilfredo"] = { latitude = -55.870673, longitude = -67.114708, display_name = ("Caleta Wilfredo")}, ["Rio Grande"] = { latitude = -53.791510, longitude = -67.704934, display_name = ("Rio Grande")}, ["Estancia Puerto Rancho"] = { latitude = -55.046937, longitude = -66.561407, display_name = ("Estancia Puerto Rancho")}, ["Vertedero"] = { latitude = -49.115832, longitude = -74.390374, display_name = ("Vertedero")}, ["Los Pioneros"] = { latitude = -51.728856, longitude = -72.476261, display_name = ("Los Pioneros")}, ["Punta Sedger"] = { latitude = -53.661658, longitude = -70.956762, display_name = ("Punta Sedger")}, ["Laguna Azul"] = { latitude = -50.853998, longitude = -72.700889, display_name = ("Laguna Azul")}, ["Gemita"] = { latitude = -50.874411, longitude = -72.703797, display_name = ("Gemita")}, ["Lago Sarmiento"] = { latitude = -51.005428, longitude = -72.599023, display_name = ("Lago Sarmiento")}, ["San Luis"] = { latitude = -51.014177, longitude = -72.500274, display_name = ("San Luis")}, ["Estancia Entre Lagos"] = { latitude = -51.065949, longitude = -72.549804, display_name = ("Estancia Entre Lagos")}, ["San Antonio"] = { latitude = -51.152146, longitude = -72.520213, display_name = ("San Antonio")}, ["Isla Lincoln"] = { latitude = -51.168967, longitude = -72.597499, display_name = ("Isla Lincoln")}, ["Albergue Chileno"] = { latitude = -50.973130, longitude = -72.875996, display_name = ("Albergue Chileno")}, ["Hotel Lago Grey"] = { latitude = -51.122880, longitude = -73.116350, display_name = ("Hotel Lago Grey")}, ["Administracia Parque Torres del Paine"] = { latitude = -51.175866, longitude = -72.954499, display_name = ("Administracia Parque Torres del Paine")}, ["Complejo Torres del Paine"] = { latitude = -51.386627, longitude = -72.794637, display_name = ("Complejo Torres del Paine")}, ["Santucci"] = { latitude = -51.307294, longitude = -72.646857, display_name = ("Santucci")}, ["Vega Castillo"] = { latitude = -51.303816, longitude = -72.647865, display_name = ("Vega Castillo")}, ["Hotel Rubens"] = { latitude = -52.039287, longitude = -71.941053, display_name = ("Hotel Rubens")}, ["Aserradero Monte Alto"] = { latitude = -52.051198, longitude = -71.894720, display_name = ("Aserradero Monte Alto")}, ["Dos Lagunas"] = { latitude = -51.511928, longitude = -72.509151, display_name = ("Dos Lagunas")}, ["La Esperanza"] = { latitude = -48.242630, longitude = -73.516136, display_name = ("La Esperanza")}, ["Fiordo Steffens"] = { latitude = -47.622366, longitude = -73.657725, display_name = ("Fiordo Steffens")}, ["La Brisca"] = { latitude = -47.807504, longitude = -73.526652, display_name = ("La Brisca")}, ["Isla San Francisco"] = { latitude = -48.067385, longitude = -73.580907, display_name = ("Isla San Francisco")}, ["Ventisquero Jorge Montt"] = { latitude = -48.194167, longitude = -73.464083, display_name = ("Ventisquero Jorge Montt")}, ["Pampa Guanaco"] = { latitude = -54.051056, longitude = -68.803126, display_name = ("Pampa Guanaco")}, ["Cabeza de Mar"] = { latitude = -52.801717, longitude = -71.001175, display_name = ("Cabeza de Mar")}, ["Guairabo"] = { latitude = -53.338356, longitude = -70.956558, display_name = ("Guairabo")}, ["Agua Fresca Norte"] = { latitude = -53.459476, longitude = -70.973166, display_name = ("Agua Fresca Norte")}, ["Arroyo El Sucio"] = { latitude = -48.191174, longitude = -72.309602, display_name = ("Arroyo El Sucio")}, ["Campamento Mayer"] = { latitude = -48.195712, longitude = -72.337717, display_name = ("Campamento Mayer")}, ["Entrada Mayer"] = { latitude = -48.209324, longitude = -72.323345, display_name = ("Entrada Mayer")}, ["Pampa Grande"] = { latitude = -48.296577, longitude = -72.493432, display_name = ("Pampa Grande")}, ["Las Claras"] = { latitude = -48.295892, longitude = -72.476684, display_name = ("Las Claras")}, ["Las Margaritas"] = { latitude = -48.305712, longitude = -72.436809, display_name = ("Las Margaritas")}, ["Pampa Chica"] = { latitude = -48.316616, longitude = -72.503967, display_name = ("Pampa Chica")}, ["La Centinela"] = { latitude = -48.395002, longitude = -72.649259, display_name = ("La Centinela")}, ["Santa Eva"] = { latitude = -48.430265, longitude = -72.635015, display_name = ("Santa Eva")}, ["Viejo Roble"] = { latitude = -48.783798, longitude = -72.759755, display_name = ("Viejo Roble")}, ["Candelario Mancilla"] = { latitude = -48.868596, longitude = -72.743227, display_name = ("Candelario Mancilla")}, ["Ventisquero Chico"] = { latitude = -48.863020, longitude = -72.897756, display_name = ("Ventisquero Chico")}, ["Las Carmelas"] = { latitude = -48.900666, longitude = -72.979865, display_name = ("Las Carmelas")}, ["Isla Central"] = { latitude = -48.774391, longitude = -72.895820, display_name = ("Isla Central")}, ["La Lezna"] = { latitude = -48.706184, longitude = -73.023382, display_name = ("La Lezna")}, ["Bajo Esperanza"] = { latitude = -48.421037, longitude = -72.980907, display_name = ("Bajo Esperanza")}, ["Laguna San Rafael"] = { latitude = -46.639497, longitude = -73.872997, display_name = ("Laguna San Rafael")}, ["Los Maitenes"] = { latitude = -46.855073, longitude = -72.379703, display_name = ("Los Maitenes")}, ["Laguna La Manga"] = { latitude = -46.877339, longitude = -72.701664, display_name = ("Laguna La Manga")}, ["Pampa Seguel"] = { latitude = -46.986004, longitude = -72.788397, display_name = ("Pampa Seguel")}, ["Lago Plomo Sur"] = { latitude = -47.010740, longitude = -72.988804, display_name = ("Lago Plomo Sur")}, ["Lago Plomo Norte"] = { latitude = -47.001796, longitude = -72.979792, display_name = ("Lago Plomo Norte")}, ["Lago Bertrand"] = { latitude = -46.901836, longitude = -72.877137, display_name = ("Lago Bertrand")}, ["Lago Negro"] = { latitude = -46.895252, longitude = -72.789063, display_name = ("Lago Negro")}, ["Ensenada Villarino"] = { latitude = -54.922173, longitude = -67.779399, display_name = ("Ensenada Villarino")}, ["Caleta Lewaia"] = { latitude = -54.929608, longitude = -68.338882, display_name = ("Caleta Lewaia")}, ["Arroyo Los Toros"] = { latitude = -47.818186, longitude = -72.730822, display_name = ("Arroyo Los Toros")}, ["Los CIpreses"] = { latitude = -47.485511, longitude = -72.940629, display_name = ("Los CIpreses")}, ["La Esperanza Sur"] = { latitude = -47.481088, longitude = -72.908613, display_name = ("La Esperanza Sur")}, ["Buena Vista Sur"] = { latitude = -47.496748, longitude = -72.894065, display_name = ("Buena Vista Sur")}, ["La Herradura"] = { latitude = -47.329806, longitude = -72.589276, display_name = ("La Herradura")}, ["Laguna Esmeralda"] = { latitude = -47.320104, longitude = -72.576745, display_name = ("Laguna Esmeralda")}, ["Puerto Nuevo"] = { latitude = -47.265819, longitude = -72.482523, display_name = ("Puerto Nuevo")}, ["Laguna Escondida"] = { latitude = -47.243732, longitude = -72.388162, display_name = ("Laguna Escondida")}, ["Frutillar"] = { latitude = -47.246107, longitude = -72.428696, display_name = ("Frutillar")}, ["Dos Arroyos"] = { latitude = -47.242130, longitude = -72.329903, display_name = ("Dos Arroyos")}, ["La Guachita"] = { latitude = -47.242450, longitude = -72.285947, display_name = ("La Guachita")}, ["La Montosa"] = { latitude = -47.299292, longitude = -72.027116, display_name = ("La Montosa")}, ["La Soledad"] = { latitude = -47.381977, longitude = -72.207754, display_name = ("La Soledad")}, ["Santa Rosa"] = { latitude = -47.465831, longitude = -72.446287, display_name = ("Santa Rosa")}, ["Seis Hermanos"] = { latitude = -47.463175, longitude = -72.542154, display_name = ("Seis Hermanos")}, ["La Pachacha"] = { latitude = -47.454064, longitude = -72.577453, display_name = ("La Pachacha")}, ["Sin Huella"] = { latitude = -47.432107, longitude = -72.589201, display_name = ("Sin Huella")}, ["La Primavera"] = { latitude = -51.447714, longitude = -72.228270, display_name = ("La Primavera")}, ["Tres Marias"] = { latitude = -51.318977, longitude = -72.176226, display_name = ("Tres Marias")}, ["La Fermina"] = { latitude = -51.690785, longitude = -72.083675, display_name = ("La Fermina")}, ["Laguna Larga"] = { latitude = -51.808214, longitude = -71.882625, display_name = ("Laguna Larga")}, ["Rincon de Los Morros"] = { latitude = -51.920101, longitude = -71.497324, display_name = ("Rincon de Los Morros")}, ["Pampa"] = { latitude = -51.949091, longitude = -71.168665, display_name = ("Pampa")}, ["Pali Aike"] = { latitude = -51.983025, longitude = -69.748869, display_name = ("Pali Aike")}, ["Osasuna"] = { latitude = -51.955927, longitude = -69.891237, display_name = ("Osasuna")}, ["Rosita"] = { latitude = -54.021954, longitude = -68.472809, display_name = ("Rosita")}, ["Timaukel"] = { latitude = -53.688797, longitude = -69.902275, display_name = ("Timaukel")}, ["Biotex"] = { latitude = -53.658004, longitude = -69.717224, display_name = ("Biotex")}, ["Los Coles"] = { latitude = -52.560165, longitude = -71.696809, display_name = ("Los Coles")}, ["Cruz del Sur"] = { latitude = -52.557386, longitude = -71.544394, display_name = ("Cruz del Sur")}, ["Estancia Santa Florencia"] = { latitude = -52.688038, longitude = -71.159152, display_name = ("Estancia Santa Florencia")}, ["Monte Bello"] = { latitude = -52.633102, longitude = -71.208709, display_name = ("Monte Bello")}, ["Puesto Galy"] = { latitude = -52.385479, longitude = -71.404517, display_name = ("Puesto Galy")}, ["Estancia Onamonte"] = { latitude = -53.989247, longitude = -69.115475, display_name = ("Estancia Onamonte")}, ["Sector Chacras"] = { latitude = -46.850571, longitude = -72.698799, display_name = ("Sector Chacras")}, ["Las Chacras"] = { latitude = -48.478452, longitude = -72.557845, display_name = ("Las Chacras")}, ["Aerodromo Enrique Mayer Soto"] = { latitude = -47.785862, longitude = -73.526357, display_name = ("Aerodromo Enrique Mayer Soto")}, ["Parque Karukinka"] = { latitude = -54.150252, longitude = -68.711751, display_name = ("Parque Karukinka")}, ["Russfin"] = { latitude = -53.750757, longitude = -69.210967, display_name = ("Russfin")}, ["Campo Lindo"] = { latitude = -53.312178, longitude = -69.460037, display_name = ("Campo Lindo")}, ["Cecilia"] = { latitude = -53.278268, longitude = -69.326345, display_name = ("Cecilia")}, ["Dorotea"] = { latitude = -51.647924, longitude = -72.339118, display_name = ("Dorotea")}, ["Huertos Dorotea"] = { latitude = -51.618157, longitude = -72.332252, display_name = ("Huertos Dorotea")}, ["Hotel Explora"] = { latitude = -51.116256, longitude = -72.990730, display_name = ("Hotel Explora")}, ["Espora"] = { latitude = -52.491211, longitude = -69.422756, display_name = ("Espora")}, ["Doce de Febrero"] = { latitude = -52.893513, longitude = -70.141170, display_name = ("Doce de Febrero")}, ["Blessen"] = { latitude = -53.079155, longitude = -70.869196, display_name = ("Blessen")}, ["Pampa Alegre"] = { latitude = -53.072890, longitude = -70.865130, display_name = ("Pampa Alegre")}, ["Estancia Zenteno"] = { latitude = -52.806512, longitude = -70.797879, display_name = ("Estancia Zenteno")}, ["Estancia Tamel Aike"] = { latitude = -52.756655, longitude = -70.810397, display_name = ("Estancia Tamel Aike")}, ["Puesto Segunda Angostura"] = { latitude = -52.648078, longitude = -70.347401, display_name = ("Puesto Segunda Angostura")}, ["Monte Aymond"] = { latitude = -52.146118, longitude = -69.516473, display_name = ("Monte Aymond")}, ["Lago Leal"] = { latitude = -48.025871, longitude = -73.122166, display_name = ("Lago Leal")}, ["Barrio Industrial"] = { latitude = -54.930718, longitude = -67.571524, display_name = ("Barrio Industrial")}, ["Paso Laurita"] = { latitude = -51.689279, longitude = -72.298001, display_name = ("Paso Laurita")}, ["Aeropuerto"] = { latitude = -53.006840, longitude = -70.830867, display_name = ("Aeropuerto")}, ["Los Naranjos"] = { latitude = -53.032203, longitude = -70.837240, display_name = ("Los Naranjos")}, ["Las Siembras"] = { latitude = -53.039477, longitude = -70.878621, display_name = ("Las Siembras")}, ["Aves del Sur"] = { latitude = -53.033232, longitude = -70.903856, display_name = ("Aves del Sur")}, ["La Cruz"] = { latitude = -53.063169, longitude = -70.869064, display_name = ("La Cruz")}, ["Pueblo Vasco"] = { latitude = -53.075704, longitude = -70.874987, display_name = ("Pueblo Vasco")}, ["Vrsalovic"] = { latitude = -53.078356, longitude = -70.887325, display_name = ("Vrsalovic")}, ["Fuerte Bulnes"] = { latitude = -53.629971, longitude = -70.918854, display_name = ("Fuerte Bulnes")}, ["Rinconada Bulnes"] = { latitude = -53.593269, longitude = -70.938812, display_name = ("Rinconada Bulnes")}, ["Discordia"] = { latitude = -53.268093, longitude = -70.944880, display_name = ("Discordia")}, ["Estancia San Pedro"] = { latitude = -53.250811, longitude = -70.953361, display_name = ("Estancia San Pedro")}, ["Natales Oriente"] = { latitude = -51.729525, longitude = -72.473877, display_name = ("Natales Oriente")}, ["Diffunta Correa"] = { latitude = -51.698075, longitude = -72.431193, display_name = ("Diffunta Correa")}, ["Estancia Asturiana"] = { latitude = -51.879054, longitude = -72.083969, display_name = ("Estancia Asturiana")}, ["Estancia Laguna Dorotea"] = { latitude = -51.548837, longitude = -72.496217, display_name = ("Estancia Laguna Dorotea")}, ["Regimiento Lanceros"] = { latitude = -51.711773, longitude = -72.439286, display_name = ("Regimiento Lanceros")}, ["Huertos Endesa"] = { latitude = -51.732968, longitude = -72.467439, display_name = ("Huertos Endesa")}, ["Isabel Riquelme"] = { latitude = -51.742595, longitude = -72.457472, display_name = ("Isabel Riquelme")}, ["Estancia La Caravana"] = { latitude = -51.818187, longitude = -72.225514, display_name = ("Estancia La Caravana")}, ["Puerto Nuevo"] = { latitude = -52.555450, longitude = -71.964865, display_name = ("Puerto Nuevo")}, ["La Frontera"] = { latitude = -53.265089, longitude = -68.716562, display_name = ("La Frontera")}, ["La Herradura"] = { latitude = -48.253982, longitude = -72.387931, display_name = ("La Herradura")}, ["Estancia Gladis"] = { latitude = -52.554967, longitude = -70.406201, display_name = ("Estancia Gladis")}, ["Piedra del Fraile"] = { latitude = -49.227558, longitude = -73.013723, display_name = ("Piedra del Fraile")}, ["Ana Navarro"] = { latitude = -53.080864, longitude = -70.889650, display_name = ("Ana Navarro")}, ["Paso del Vuelto"] = { latitude = -49.821923, longitude = -73.850491, display_name = ("Paso del Vuelto")}, ["Estancia Perales"] = { latitude = -51.542409, longitude = -72.843228, display_name = ("Estancia Perales")}, ["Estancia La Portada"] = { latitude = -52.235789, longitude = -70.275169, display_name = ("Estancia La Portada")}, ["Estancia La Portada"] = { latitude = -52.072955, longitude = -70.195828, display_name = ("Estancia La Portada")}, ["Puesto Treinta y Siete"] = { latitude = -52.055970, longitude = -70.536943, display_name = ("Puesto Treinta y Siete")}, ["La Vega"] = { latitude = -52.289937, longitude = -70.531644, display_name = ("La Vega")}, ["Estancia Oasy Harbour"] = { latitude = -52.516535, longitude = -70.431899, display_name = ("Estancia Oasy Harbour")}, ["Brazo Norte"] = { latitude = -52.056235, longitude = -70.005431, display_name = ("Brazo Norte")}, ["Estancia Rose Aike Sur"] = { latitude = -52.284494, longitude = -70.114229, display_name = ("Estancia Rose Aike Sur")}, ["Estancia Los Coipos"] = { latitude = -52.678978, longitude = -70.786135, display_name = ("Estancia Los Coipos")}, ["Estancia Avelina"] = { latitude = -52.573300, longitude = -70.772019, display_name = ("Estancia Avelina")}, ["Estancia Los Cisnes"] = { latitude = -52.620034, longitude = -70.825887, display_name = ("Estancia Los Cisnes")}, ["Estancia Tres Chorrillos"] = { latitude = -52.519286, longitude = -70.716552, display_name = ("Estancia Tres Chorrillos")}, ["Estancia Divina Esperanza"] = { latitude = -52.321467, longitude = -71.042790, display_name = ("Estancia Divina Esperanza")}, ["Estancia Leona English"] = { latitude = -52.389336, longitude = -71.048506, display_name = ("Estancia Leona English")}, ["Estancia Penitente"] = { latitude = -52.119768, longitude = -71.384551, display_name = ("Estancia Penitente")}, ["Estancia Macerena"] = { latitude = -53.094961, longitude = -71.195519, display_name = ("Estancia Macerena")}, ["Marcela"] = { latitude = -53.475945, longitude = -71.195189, display_name = ("Marcela")}, ["Estancia Mimica"] = { latitude = -52.766262, longitude = -69.521196, display_name = ("Estancia Mimica")}, ["Estancia Mercedes"] = { latitude = -53.395959, longitude = -70.311980, display_name = ("Estancia Mercedes")}, ["Puerto Paredes"] = { latitude = -55.091295, longitude = -70.682970, display_name = ("Puerto Paredes")}, ["Puerto Chico"] = { latitude = -55.094981, longitude = -70.618452, display_name = ("Puerto Chico")}, ["Caleta Buen Refugio"] = { latitude = -52.836867, longitude = -74.576989, display_name = ("Caleta Buen Refugio")}, ["Punta Segunda"] = { latitude = -52.090345, longitude = -73.096054, display_name = ("Punta Segunda")}, ["Punta Barranco"] = { latitude = -52.091255, longitude = -73.098177, display_name = ("Punta Barranco")}, ["Punta Kirke Sur"] = { latitude = -52.101023, longitude = -73.120687, display_name = ("Punta Kirke Sur")}, ["Punta Boca"] = { latitude = -52.086456, longitude = -73.109958, display_name = ("Punta Boca")}, ["Punta Benitez"] = { latitude = -52.078069, longitude = -73.081548, display_name = ("Punta Benitez")}, ["Punta Alta"] = { latitude = -52.085335, longitude = -73.082577, display_name = ("Punta Alta")}, ["Punta Seca"] = { latitude = -52.078425, longitude = -73.058202, display_name = ("Punta Seca")}, ["Punta Cono"] = { latitude = -52.073097, longitude = -73.048031, display_name = ("Punta Cono")}, ["Punta Pasaje"] = { latitude = -52.066581, longitude = -73.032796, display_name = ("Punta Pasaje")}, ["Punta Medio"] = { latitude = -52.065025, longitude = -73.025456, display_name = ("Punta Medio")}, ["Punta Escoben"] = { latitude = -52.055789, longitude = -73.019878, display_name = ("Punta Escoben")}, ["Caleta Zorro"] = { latitude = -52.065605, longitude = -73.010522, display_name = ("Caleta Zorro")}, ["Punta Entrada"] = { latitude = -52.060196, longitude = -73.003398, display_name = ("Punta Entrada")}, ["Punta Restinga"] = { latitude = -52.059833, longitude = -73.009986, display_name = ("Punta Restinga")}, ["Angostura Kirke"] = { latitude = -52.058432, longitude = -73.010308, display_name = ("Angostura Kirke")}, ["Costa Centro"] = { latitude = -54.808705, longitude = -68.304695, display_name = ("Costa Centro")}, ["Fundo San Fernando"] = { latitude = -53.516299, longitude = -70.963340, display_name = ("Fundo San Fernando")}, ["China Creek"] = { latitude = -53.154076, longitude = -69.182848, display_name = ("China Creek")}, ["Grota Antonio Gil"] = { latitude = -51.500582, longitude = -72.252174, display_name = ("Grota Antonio Gil")}, ["Estancia Entre Rios"] = { latitude = -48.265328, longitude = -72.218942, display_name = ("Estancia Entre Rios")}, ["Punta Vergara"] = { latitude = -51.928689, longitude = -72.576822, display_name = ("Punta Vergara")}, ["Ladrilleros"] = { latitude = -51.834170, longitude = -72.725600, display_name = ("Ladrilleros")}, ["San Luis"] = { latitude = -51.690244, longitude = -72.641448, display_name = ("San Luis")}, ["Esmeralda"] = { latitude = -53.622372, longitude = -70.475991, display_name = ("Esmeralda")}, ["Estancia Kerber"] = { latitude = -52.092902, longitude = -72.059502, display_name = ("Estancia Kerber")}, ["Estancia San Lucas"] = { latitude = -52.143330, longitude = -72.059988, display_name = ("Estancia San Lucas")}, ["Estancia Las Nieves"] = { latitude = -52.305619, longitude = -71.840212, display_name = ("Estancia Las Nieves")}, ["Puerto Almanza"] = { latitude = -54.870409, longitude = -67.564406, display_name = ("Puerto Almanza")}, ["Estancia Moat"] = { latitude = -54.973617, longitude = -66.745034, display_name = ("Estancia Moat")}, ["Dungeness"] = { latitude = -52.394832, longitude = -68.431165, display_name = ("Dungeness")}, ["Asturiana"] = { latitude = -51.879134, longitude = -72.083448, display_name = ("Asturiana")}, ["Isabel Riquelme"] = { latitude = -51.871646, longitude = -72.465547, display_name = ("Isabel Riquelme")}, ["Rotunda"] = { latitude = -51.964359, longitude = -72.584992, display_name = ("Rotunda")}, ["Lago Pinto"] = { latitude = -52.102842, longitude = -72.412986, display_name = ("Lago Pinto")}, ["Loteo Las Flores"] = { latitude = -53.055261, longitude = -70.867235, display_name = ("Loteo Las Flores")}, ["Romero"] = { latitude = -52.356660, longitude = -70.444921, display_name = ("Romero")}, ["Lago Vargas"] = { latitude = -47.697060, longitude = -73.076762, display_name = ("Lago Vargas")}, ["Puesto Buque Quemado"] = { latitude = -52.407205, longitude = -69.568150, display_name = ("Puesto Buque Quemado")}, ["Puesto Cerro Negro"] = { latitude = -52.400557, longitude = -69.641565, display_name = ("Puesto Cerro Negro")}, ["Puesto Rancho Carancho"] = { latitude = -52.360647, longitude = -69.487559, display_name = ("Puesto Rancho Carancho")}, ["Interior Dungeness"] = { latitude = -52.339613, longitude = -68.469438, display_name = ("Interior Dungeness")}, ["Las Barrancas"] = { latitude = -52.318940, longitude = -68.587263, display_name = ("Las Barrancas")}, ["Daniel Uno"] = { latitude = -52.342340, longitude = -68.666549, display_name = ("Daniel Uno")}, ["Catalina Dos"] = { latitude = -52.375862, longitude = -68.787445, display_name = ("Catalina Dos")}, ["Catalina Uno"] = { latitude = -52.390295, longitude = -68.787268, display_name = ("Catalina Uno")}, ["Anguila"] = { latitude = -52.409154, longitude = -68.784227, display_name = ("Anguila")}, ["Puesto Alamiro"] = { latitude = -52.228077, longitude = -69.023152, display_name = ("Puesto Alamiro")}, ["Puesto Chocolate"] = { latitude = -52.176616, longitude = -69.453188, display_name = ("Puesto Chocolate")}, ["Puesto Manantial Alto"] = { latitude = -52.265334, longitude = -69.545810, display_name = ("Puesto Manantial Alto")}, ["Puesto Monte Aymond"] = { latitude = -52.152339, longitude = -69.595839, display_name = ("Puesto Monte Aymond")}, ["Puesto Caravana"] = { latitude = -52.212071, longitude = -69.698369, display_name = ("Puesto Caravana")}, ["Puesto La Calle"] = { latitude = -52.225946, longitude = -69.985221, display_name = ("Puesto La Calle")}, ["Puesto Pampa Larga"] = { latitude = -52.231292, longitude = -69.828837, display_name = ("Puesto Pampa Larga")}, ["Puesto Roci Aike"] = { latitude = -52.187763, longitude = -70.177161, display_name = ("Puesto Roci Aike")}, ["Las Barrancas"] = { latitude = -52.167131, longitude = -70.266302, display_name = ("Las Barrancas")}, ["Puesto Teniente Merino"] = { latitude = -52.143342, longitude = -70.452375, display_name = ("Puesto Teniente Merino")}, ["Puesto Vargas"] = { latitude = -52.159971, longitude = -70.532484, display_name = ("Puesto Vargas")}, ["Puesto Final"] = { latitude = -52.157786, longitude = -70.535451, display_name = ("Puesto Final")}, ["Puesto El Centro"] = { latitude = -52.091769, longitude = -70.417326, display_name = ("Puesto El Centro")}, ["Puesto Cuarenta y Cuatro"] = { latitude = -52.074871, longitude = -70.860037, display_name = ("Puesto Cuarenta y Cuatro")}, ["Lagunita"] = { latitude = -52.110769, longitude = -71.163455, display_name = ("Lagunita")}, ["Puesto Lagunita"] = { latitude = -52.121721, longitude = -71.171534, display_name = ("Puesto Lagunita")}, ["Puesto Caravana"] = { latitude = -52.115203, longitude = -71.276688, display_name = ("Puesto Caravana")}, ["Ann Aiken"] = { latitude = -52.061522, longitude = -71.230210, display_name = ("Ann Aiken")}, ["Puesto Ann Aiken"] = { latitude = -52.088304, longitude = -71.274698, display_name = ("Puesto Ann Aiken")}, ["Puesto Mulato"] = { latitude = -52.018599, longitude = -71.252006, display_name = ("Puesto Mulato")}, ["Puesto Palermo"] = { latitude = -52.062465, longitude = -71.426839, display_name = ("Puesto Palermo")}, ["Palermo"] = { latitude = -52.128193, longitude = -71.403654, display_name = ("Palermo")}, ["Puesto Bishop"] = { latitude = -52.132876, longitude = -71.406739, display_name = ("Puesto Bishop")}, ["Vilicic"] = { latitude = -52.134897, longitude = -71.555307, display_name = ("Vilicic")}, ["Puesto Morro Chico"] = { latitude = -52.048631, longitude = -71.553665, display_name = ("Puesto Morro Chico")}, ["Puesto Pioneros"] = { latitude = -52.026759, longitude = -71.540790, display_name = ("Puesto Pioneros")}, ["Seno Otway Norte"] = { latitude = -52.803131, longitude = -71.173937, display_name = ("Seno Otway Norte")}, ["Estancia Laguna Toro"] = { latitude = -52.793563, longitude = -71.147040, display_name = ("Estancia Laguna Toro")}, ["Parador Laguna Blanca"] = { latitude = -52.799794, longitude = -71.190625, display_name = ("Parador Laguna Blanca")}, ["Puesto La Calle"] = { latitude = -52.620097, longitude = -71.146565, display_name = ("Puesto La Calle")}, ["Mina Invierno"] = { latitude = -52.885486, longitude = -71.611374, display_name = ("Mina Invierno")}, ["Estancia Adela"] = { latitude = -52.850396, longitude = -71.510362, display_name = ("Estancia Adela")}, ["Puesto Vilicic"] = { latitude = -52.802563, longitude = -71.402667, display_name = ("Puesto Vilicic")}, ["Puesto Viejo"] = { latitude = -52.723083, longitude = -71.551014, display_name = ("Puesto Viejo")}, ["Estancia del Pilar"] = { latitude = -52.710677, longitude = -71.423951, display_name = ("Estancia del Pilar")}, ["Estancia La Reina"] = { latitude = -52.761944, longitude = -71.354814, display_name = ("Estancia La Reina")}, ["Puesto Las Charas"] = { latitude = -52.689612, longitude = -71.416774, display_name = ("Puesto Las Charas")}, ["Puesto La Laguna"] = { latitude = -52.506730, longitude = -71.208715, display_name = ("Puesto La Laguna")}, ["Puesto Borrego Uno"] = { latitude = -52.524688, longitude = -71.318402, display_name = ("Puesto Borrego Uno")}, ["Puesto Dieciocho"] = { latitude = -52.446408, longitude = -71.442470, display_name = ("Puesto Dieciocho")}, ["Puesto de Arrieros"] = { latitude = -52.396285, longitude = -71.410879, display_name = ("Puesto de Arrieros")}, ["Refugio de Arrieros"] = { latitude = -52.280578, longitude = -71.347799, display_name = ("Refugio de Arrieros")}, ["Puesto Bus"] = { latitude = -52.297255, longitude = -71.452035, display_name = ("Puesto Bus")}, ["Macarena"] = { latitude = -52.287854, longitude = -71.613371, display_name = ("Macarena")}, ["Rancho Salazar Uno"] = { latitude = -52.402236, longitude = -71.795105, display_name = ("Rancho Salazar Uno")}, ["Rancho Salazar Dos"] = { latitude = -52.495899, longitude = -71.693263, display_name = ("Rancho Salazar Dos")}, ["Puesto Martinic"] = { latitude = -52.551349, longitude = -71.826623, display_name = ("Puesto Martinic")}, ["Puesto Nutria"] = { latitude = -52.523961, longitude = -72.127942, display_name = ("Puesto Nutria")}, ["Isla Marta"] = { latitude = -52.556943, longitude = -72.298979, display_name = ("Isla Marta")}, ["Surgidero Furia"] = { latitude = -52.625027, longitude = -72.410892, display_name = ("Surgidero Furia")}, ["Isla Unicornio"] = { latitude = -52.631631, longitude = -72.373973, display_name = ("Isla Unicornio")}, ["Isla Unicornio Sur"] = { latitude = -52.638090, longitude = -72.396043, display_name = ("Isla Unicornio Sur")}, ["Punta Laura"] = { latitude = -52.649443, longitude = -72.403982, display_name = ("Punta Laura")}, ["Mardin"] = { latitude = -52.651056, longitude = -72.415392, display_name = ("Mardin")}, ["Condominio Vista Norte"] = { latitude = -53.096710, longitude = -70.881977, display_name = ("Condominio Vista Norte")}, ["Parroquia de Barranco Amarillo"] = { latitude = -53.094565, longitude = -70.877653, display_name = ("Parroquia de Barranco Amarillo")}, ["Gruta Virgen de Covadonga"] = { latitude = -53.053915, longitude = -70.891379, display_name = ("Gruta Virgen de Covadonga")}, ["Laguna de Los Cisnes"] = { latitude = -53.274474, longitude = -70.367704, display_name = ("Laguna de Los Cisnes")}, ["Lomas del Baquedano"] = { latitude = -53.289182, longitude = -70.358475, display_name = ("Lomas del Baquedano")}, ["Futuro Patagonia"] = { latitude = -53.291369, longitude = -70.378248, display_name = ("Futuro Patagonia")}, ["Sector Las Siembras"] = { latitude = -53.035698, longitude = -70.899996, display_name = ("Sector Las Siembras")}, ["Glaciar Grey"] = { latitude = -51.738356, longitude = -72.474903, display_name = ("Glaciar Grey")}, ["Glaciar Serrano"] = { latitude = -51.738108, longitude = -72.472936, display_name = ("Glaciar Serrano")}, ["Punta Arenas"] = { latitude = -53.155050, longitude = -70.922954, display_name = ("Punta Arenas")}, ["Rio Gallegos"] = { latitude = -51.625855, longitude = -69.223086, display_name = ("Rio Gallegos")}, } --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- Module loading log --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veaf.loggers.get(veafNamedPoints.Id):info(string.format("Loading version %s", veafNamedPoints.Version)) --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- MODULE TESTS --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --[[ veaf.loggers.get(veafNamedPoints.Id):trace("MODULE TESTS: " .. veafNamedPoints.Id) veafNamedPoints.addCities() veafNamedPoints.addAirbases() veaf.loggers.get(veafNamedPoints.Id):trace(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") for _, veafNamedPoint in pairs(veafNamedPoints.namedPoints) do if (veaf.startsWith(_, "AIRBASE", false)) then veaf.loggers.get(veafNamedPoints.Id):trace(">>> %s", veaf.p(veafNamedPoint)) end --veaf.loggers.get(veafNamedPoints.Id):trace(">>> %s", veaf.p(veafNamedPoint)) veafNamedPoints.markid = veafNamedPoints.markid + 1 trigger.action.markToAll(veafNamedPoints.markid, "VEAF - Point named ".. _, veafNamedPoint, true) end veaf.loggers.get(veafNamedPoints.Id):trace(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") ]] ------------------ END script veafNamedPoints.lua ------------------ ------------------ START script veafQraManager.lua ------------------ ------------------------------------------------------------------ -- VEAF Quick Reaction Alert for DCS World -- https://en.wikipedia.org/wiki/Quick_Reaction_Alert -- By Zip (2020) and Rex (2022) -- -- Features: -- --------- -- * Define zones that are defended by an AI flight -- * Default behavior: when an ennemy aircraft enters the zone, QRA patrol is spawned; then, when it is destroyed, the zone is not defended anymore; when all enemy aircrafts have left the zone, it resets and can respawn a new QRA -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafQraManager = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafQraManager.Id = "QRA" --- Version. veafQraManager.Version = "1.2.4" -- trace level, specific to this module --veafQraManager.LogLevel = "trace" veaf.loggers.new(veafQraManager.Id, veafQraManager.LogLevel) function veafQraManager.statusToString(status) if status == veafQraManager.STATUS_WILLREARM then return "STATUS_WILLREARM" end if status == veafQraManager.STATUS_READY then return "STATUS_READY" end if status == veafQraManager.STATUS_READY_WAITINGFORMORE then return "STATUS_READY_WAITINGFORMORE" end if status == veafQraManager.STATUS_ACTIVE then return "STATUS_ACTIVE" end if status == veafQraManager.STATUS_DEAD then return "STATUS_DEAD" end return "" end veafQraManager.STATUS_WILLREARM = 0 veafQraManager.STATUS_READY = 1 veafQraManager.STATUS_READY_WAITINGFORMORE = 1.5 veafQraManager.STATUS_ACTIVE = 2 veafQraManager.STATUS_DEAD = 3 --scheduled states veafQraManager.STATUS_OUT = 4 veafQraManager.STATUS_NOAIRBASE = 5 veafQraManager.STATUS_STOP = 6 veafQraManager.WATCHDOG_DELAY = 5 veafQraManager.MINIMUM_LIFE_FOR_QRA_IN_PERCENT = 10 veafQraManager.DEFAULT_airbaseMinLifePercent = 0.9 veafQraManager.AllSilence = false --value to set all spawned QRAs to silent if true. By default it's false but this value can be set in the missionConfig veafQraManager.DEFAULT_MESSAGE_START = "%s is online" veafQraManager.DEFAULT_MESSAGE_DEPLOY = "%s is deploying" veafQraManager.DEFAULT_MESSAGE_DESTROYED = "%s has been destroyed" veafQraManager.DEFAULT_MESSAGE_READY = "%s is ready" veafQraManager.DEFAULT_MESSAGE_OUT = "%s is out of aircrafts" veafQraManager.DEFAULT_MESSAGE_RESUPPLIED = "%s has been resupplied" veafQraManager.DEFAULT_MESSAGE_AIRBASE_DOWN = "%s lost it's airbase" veafQraManager.DEFAULT_MESSAGE_AIRBASE_UP = "%s now has an airbase" veafQraManager.DEFAULT_MESSAGE_STOP = "%s is offline" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafQraManager.qras = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafQRA class methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafQRA = {} function VeafQRA.init(object) -- technical name (QRA instance name) object.name = nil -- trigger zone name (if set, we'll use a DCS trigger zone) object.triggerZoneName = nil -- center (point in the center of the circle, when not using a DCS trigger zone) object.zoneCenter = nil -- radius (size of the circle, when not using a zone) object.zoneRadius = nil -- draw the zone on screen object.drawZone = false -- description for the briefing object.description = nil -- aircraft groups forming the QRA object.groups = {} -- aircraft groups forming the QRA, in a table by enemy quantity (i.e. if this number of enemies are in the zone, spawn these groups) object.groupsToDeployByEnemyQuantity = {} -- coalition for the QRA object.coalition = nil -- coalitions the QRA is defending against object.enemyCoalitions = {} -- message when the QRA is started object.messageStart = veafQraManager.DEFAULT_MESSAGE_START -- event when the QRA is started object.onStart = nil -- message when the QRA is triggered object.messageDeploy = veafQraManager.DEFAULT_MESSAGE_DEPLOY -- event when the QRA is triggered object.onDeploy = nil -- message when the QRA is destroyed object.messageDestroyed = veafQraManager.DEFAULT_MESSAGE_DESTROYED -- event when the QRA is destroyed object.onDestroyed = nil -- message when the QRA is ready object.messageReady = veafQraManager.DEFAULT_MESSAGE_READY -- event when the QRA is ready object.onReady = nil -- message when the QRA is out of aircrafts object.messageOut = veafQraManager.DEFAULT_MESSAGE_OUT -- event when the QRA is out of aircrafts object.onOut = nil -- message when the QRA has been resupplied and will start operations against object.messageResupplied = veafQraManager.DEFAULT_MESSAGE_RESUPPLIED -- event when the QRA has been resupplied and will start operations against object.onResupplied = nil -- message when the QRA has lost the airbase it operates from object.messageAirbaseDown = veafQraManager.DEFAULT_MESSAGE_AIRBASE_DOWN -- event when the QRA has lost the airbase it operates from object.onAirbaseDown = nil -- message when the QRA has retrieved the airbase it operates from and will start operations again object.messageAirbaseUp = veafQraManager.DEFAULT_MESSAGE_AIRBASE_UP -- event when the QRA has retrieved the airbase it operates from and will start operations again object.onAirbaseUp = nil -- message when the QRA is stopped object.messageStop = veafQraManager.DEFAULT_MESSAGE_STOP -- event when the QRA is stopped object.onStop = nil -- silent means no message is emitted object.silent = veafQraManager.AllSilence -- default position for respawns (im meters, lat/lon, relative to the zone center) object.respawnDefaultOffset = {latDelta=0, lonDelta=0} -- radius of the defenders groups spawn object.respawnRadius = 250 -- reacts when helicopters enter the zone object.reactOnHelicopters = false -- delay before activating object.delayBeforeActivating = -1 -- delay before rearming object.delayBeforeRearming = -1 -- the enemy does not have to leave the zone before the QRA is rearmed object.noNeedToLeaveZoneBeforeRearming = false -- reset the QRA immediately if all the enemy units leave the zone object.resetWhenLeavingZone = false -- maximum number of QRA ready for action at once, -1 indicates infinite object.QRAmaxCount = -1 -- number of groups of aircrafts that can be spawned for this QRA in total, -1 indicates infinite. object.QRAcount = -1 -- delay in minutes before the QRA counter is increased by one, simulating some sort of logistic chain of aircrafts. object.delayBeforeQRAresupply = 0 -- maximum number of resupplies at a given time, simulating some sort of warehousing, -1 indicates infinite. Is decremented every time a resupply happens. 0 indicates no resupply. object.QRAresupplyMax = -1 -- minimum QRAcount that will trigger a resupply, -1 indicates as soon as an aircraft is lost object.QRAminCountforResupply = -1 -- how many aircraft groups are resupplied at once object.resupplyAmount = 1 -- indicator to know if the QRA is being resupplied or not object.isResupplying = false -- name of the airport to which the QRA is linked, QRAs will be deployed only if this is set and the airport is captured by the QRA's coalition or if this is not set object.airportLink = nil -- minimum linked airbase life percentage (from 0 to 1) for the QRA to have it's airbase available object.airportMinLifePercent = veafQraManager.DEFAULT_airbaseMinLifePercent -- boolean to know if the status OUT was announced or not object.outAnnounced = false -- boolean to know if the status NOAIRBASE was announced or not object.noAB_announced = false -- minimum number of enemies in the zone to trigger deployment; updated automatically by setGroupsToDeployByEnemyQuantity object.minimumNbEnemyPlanes = -1 -- planes in the zone will only be detected below this altitude (in feet) object.minimumAltitude = -999999 -- planes in the zone will only be detected above this altitude (in feet) object.maximumAltitude = 999999 object.timer = nil object.state = nil object.scheduled_state = nil object._enemyHumanUnits = nil object.spawnedGroupsNames = {} end function VeafQRA.ToggleAllSilence(state) if state then veafQraManager.AllSilence = true else veafQraManager.AllSilence = false end end function VeafQRA:new(objectToCopy) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA:new()") local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object VeafQRA.init(objectToCreate) return objectToCreate end function VeafQRA:setName(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[]:setName(%s)", veaf.p(value)) self.name = value return veafQraManager.add(self) -- add the QRA to the QRA list as soon as a name is available to index it end function VeafQRA:setTriggerZone(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setTriggerZone(%s)", veaf.p(self.name), veaf.p(value)) self.triggerZoneName = value return self end function VeafQRA:setZoneCenter(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setZoneCenter(%s)", veaf.p(self.name), veaf.p(value)) self.zoneCenter = value return self end function VeafQRA:setZoneCenterFromCoordinates(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setZoneCenterFromCoordinates(%s)", veaf.p(self.name), veaf.p(value)) local _lat, _lon = veaf.computeLLFromString(value) local vec3 = coord.LLtoLO(_lat, _lon) return self:setZoneCenter(vec3) end function VeafQRA:setZoneRadius(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setZoneRadius(%s)", veaf.p(self.name), veaf.p(value)) self.zoneRadius = value return self end function VeafQRA:setDescription(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setDescription(%s)", veaf.p(self.name), veaf.p(value)) self.description = value return veafQraManager.add(self) -- add the QRA to the QRA list as soon as a name is available to index it end function VeafQRA:getDescription() return self.description or self.name end function VeafQRA:getName() return self.name or self.description end function VeafQRA:addGroup(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:addGroup(%s)", veaf.p(self.name), veaf.p(value)) if not self.groupsToDeployByEnemyQuantity[1] then self.groupsToDeployByEnemyQuantity[1] = {} end table.insert(self.groupsToDeployByEnemyQuantity[1], value) return self end function VeafQRA:addRandomGroup(groups, number, bias) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:addRandomGroup(%s, %s, %s)", veaf.p(self.name), veaf.p(groups), veaf.p(number), veaf.p(bias)) return self:addGroup({groups, number or 1, bias or 0}) end function VeafQRA:setGroupsToDeployByEnemyQuantity(enemyNb, groupsToDeploy) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setGroupsToDeployByEnemyQuantity(%s) -> %s", veaf.p(self.name), veaf.p(enemyNb), veaf.p(groupsToDeploy)) self.groupsToDeployByEnemyQuantity[enemyNb] = groupsToDeploy if self.minimumNbEnemyPlanes == -1 or self.minimumNbEnemyPlanes > enemyNb then self.minimumNbEnemyPlanes = enemyNb end return self end function VeafQRA:setRandomGroupsToDeployByEnemyQuantity(enemyNb, groups, number, bias) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setRandomGroupsToDeployByEnemyQuantity(%s, %s, %s, %s)", veaf.p(self.name), veaf.p(enemyNb), veaf.p(groups), veaf.p(number), veaf.p(bias)) return self:setGroupsToDeployByEnemyQuantity(enemyNb, {groups, number or 1, bias or 0}) end function VeafQRA:setCoalition(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setCoalition(%s)", veaf.p(self.name), veaf.p(value)) self.coalition = value return self end function VeafQRA:addEnnemyCoalition(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:addEnnemyCoalition(%s)", veaf.p(self.name), veaf.p(value)) self.enemyCoalitions[value] = value return self end function VeafQRA:getEnnemyCoalition() local result = nil for coalition, _ in pairs(self.enemyCoalitions) do result = coalition break end return result end function VeafQRA:setMessageStart(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageStart(%s)", veaf.p(self.name), veaf.p(value)) self.messageStart = value return self end function VeafQRA:setOnStart(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnStart()", veaf.p(self.name)) self.onStart = value return self end function VeafQRA:setMessageDeploy(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageDeploy(%s)", veaf.p(self.name), veaf.p(value)) self.messageDeploy = value return self end function VeafQRA:setOnDeploy(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnDeploy()", veaf.p(self.name)) self.onDeploy = value return self end function VeafQRA:setMessageDestroyed(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageDestroyed(%s)", veaf.p(self.name), veaf.p(value)) self.messageDestroyed = value return self end function VeafQRA:setOnDestroyed(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnDestroyed()", veaf.p(self.name)) self.onDestroyed = value return self end function VeafQRA:setMessageReady(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageReady(%s)", veaf.p(self.name), veaf.p(value)) self.messageReady = value return self end function VeafQRA:setOnReady(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnReady()", veaf.p(self.name)) self.onReady = value return self end function VeafQRA:setMessageOut(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageOut(%s)", veaf.p(self.name), veaf.p(value)) self.messageOut = value return self end function VeafQRA:setOnOut(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnOut()", veaf.p(self.name)) self.onOut = value return self end function VeafQRA:setMessageResupplied(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageResupplied(%s)", veaf.p(self.name), veaf.p(value)) self.messageResupplied = value return self end function VeafQRA:setOnResupplied(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnResupplied()", veaf.p(self.name)) self.onResupplied = value return self end function VeafQRA:setMessageAirbaseDown(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageAirbaseDown(%s)", veaf.p(self.name), veaf.p(value)) self.messageAirbaseDown = value return self end function VeafQRA:setOnAirbaseDown(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnAirbaseDown()", veaf.p(self.name)) self.onAirbaseDown = value return self end function VeafQRA:setMessageAirbaseUp(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageAirbaseUp(%s)", veaf.p(self.name), veaf.p(value)) self.messageAirbaseUp = value return self end function VeafQRA:setOnAirbaseUp(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnAirbaseUp()", veaf.p(self.name)) self.onAirbaseUp = value return self end function VeafQRA:setMessageStop(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageStop(%s)", veaf.p(self.name), veaf.p(value)) self.messageStop = value return self end function VeafQRA:setOnStop(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnStop()", veaf.p(self.name)) self.onStop = value return self end function VeafQRA:setSilent(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setSilent(%s)", veaf.p(self.name), veaf.p(value)) self.silent = value or false return self end function VeafQRA:setDrawZone(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setDrawZone(%s)", veaf.p(self.name), veaf.p(value)) self.drawZone = value or false return self end --TODO, warehousing for each group within a QRA and not just the whole QRA function VeafQRA:setQRAcount(count) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setQRAcount(%s)", veaf.p(self.name), veaf.p(count)) if count and type(count) == 'number' and count >= -1 then self.QRAcount = count end return self end function VeafQRA:setQRAmaxCount(maxCount) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setQRAmaxCount(%s)", veaf.p(self.name), veaf.p(maxCount)) if maxCount and type(maxCount) == 'number' and maxCount >= -1 then self.QRAmaxCount = maxCount end return self end function VeafQRA:setQRAresupplyDelay(resupplyDelay) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setQRAresupplyDelay(%s)", veaf.p(self.name), veaf.p(resupplyDelay)) if resupplyDelay and type(resupplyDelay) == 'number' and resupplyDelay >= 0 then self.delayBeforeQRAresupply = resupplyDelay end return self end function VeafQRA:setQRAmaxResupplyCount(maxResupplyCount) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setQRAmaxResupplyCount(%s)", veaf.p(self.name), veaf.p(maxResupplyCount)) if maxResupplyCount and type(maxResupplyCount) == 'number' and maxResupplyCount >= -1 then self.QRAresupplyMax = maxResupplyCount end return self end function VeafQRA:setQRAminCountforResupply(minCountforResupply) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setQRAminCountforResupply(%s)", veaf.p(self.name), veaf.p(minCountforResupply)) if minCountforResupply and type(minCountforResupply) == 'number' and minCountforResupply >= -1 and minCountforResupply ~= 0 then self.QRAminCountforResupply = minCountforResupply end return self end function VeafQRA:setResupplyAmount(resupplyAmount) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setResupplyAmount(%s)", veaf.p(self.name), veaf.p(resupplyAmount)) if resupplyAmount and type(resupplyAmount) == 'number' and resupplyAmount >= 1 then self.resupplyAmount = resupplyAmount end return self end function VeafQRA:setAirportLink(airport_name) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setAirportLink(%s)", veaf.p(self.name), veaf.p(airport_name)) if airport_name and type(airport_name) == 'string' and Airbase.getByName(airport_name) then self.airportLink = airport_name end return self end function VeafQRA:setAirportMinLifePercent(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setAirportMinLifePercent(%s)", veaf.p(self.name), veaf.p(value)) if value and value >= 0 and value <= 1 then self.airportMinLifePercent = value end return self end function VeafQRA:setReactOnHelicopters() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setReactOnHelicopters()", veaf.p(self.name)) self.reactOnHelicopters = true return self end ---set the default respawn offset (in meters, relative to the zone center) ---@param defaultOffsetLatitude any in meters ---@param defaultOffsetLongitude any in meters ---@return table self function VeafQRA:setRespawnDefaultOffset(defaultOffsetLatitude, defaultOffsetLongitude) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setRespawnDefaultOffset(%s, %s)", veaf.p(self.name), veaf.p(defaultOffsetLatitude), veaf.p(defaultOffsetLongitude)) self.respawnDefaultOffset = { latDelta = defaultOffsetLatitude, lonDelta = defaultOffsetLongitude} return self end function VeafQRA:setRespawnRadius(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setRespawnRadius(%s)", veaf.p(self.name), veaf.p(value)) self.respawnRadius = value if self.respawnRadius < 250 then self.respawnRadius = 250 end return self end function VeafQRA:setDelayBeforeRearming(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setDelayBeforeRearming(%s)", veaf.p(self.name), veaf.p(value)) self.delayBeforeRearming = value return self end function VeafQRA:setNoNeedToLeaveZoneBeforeRearming() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setNoNeedToLeaveZoneBeforeRearming()", veaf.p(self.name)) self.noNeedToLeaveZoneBeforeRearming = true return self end function VeafQRA:setResetWhenLeavingZone() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setResetWhenLeavingZone()", veaf.p(self.name)) self.resetWhenLeavingZone = true return self end function VeafQRA:setDelayBeforeActivating(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setDelayBeforeActivating(%s)", veaf.p(self.name), veaf.p(value)) self.delayBeforeActivating = value return self end function VeafQRA:setMinimumAltitudeInFeet(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMinimumAltitudeInFeet(%s)", veaf.p(self.name), veaf.p(value)) self.minimumAltitude = value * 0.3048 -- convert from feet return self end function VeafQRA:getMinimumAltitudeInMeters() return self.minimumAltitude end function VeafQRA:setMaximumAltitudeInFeet(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMaximumAltitudeInFeet(%s)", veaf.p(self.name), veaf.p(value)) self.maximumAltitude = value * 0.3048 -- convert from feet return self end function VeafQRA:getMaximumAltitudeInMeters() return self.maximumAltitude end function VeafQRA:humanBornEvent(unit) veaf.loggers.get(veafQraManager.Id):trace("VeafQRA[%s]:humanBornEvent(%s)", self.name, unit) if not self._enemyHumanUnits then return -- do this later ^^ end local coalitionId = 0 if unit.unitCoalition then coalitionId = unit.unitCoalition end if self.enemyCoalitions[coalitionId] then veaf.loggers.get(veafQraManager.Id):trace("VeafQRA[%s]:humanBornEvent() - unit being born is an enemy (coalition %s)", self.name, coalitionId) if unit.unitCategory then if (unit.unitCategory == Unit.Category.AIRPLANE) or (unit.unitCategory == Unit.Category.HELICOPTER and self.reactOnHelicopters) then -- check if the unit is already in the list for _, unitName in pairs(self._enemyHumanUnits) do if unitName == unit.unitName then return end end veaf.loggers.get(veafQraManager.Id):trace("adding unit to enemy human units for QRA") table.insert(self._enemyHumanUnits, unit.unitName) end end end end function VeafQRA:_getEnemyHumanUnits() if not self._enemyHumanUnits then veaf.loggers.get(veafQraManager.Id):trace("VeafQRA[%s]:_getEnemyHumanUnits() - computing", veaf.p(self.name)) self._enemyHumanUnits = {} for _, unit in pairs(mist.DBs.humansByName) do local coalitionId = 0 if unit.coalition then if unit.coalition:lower() == "red" then coalitionId = coalition.side.RED elseif unit.coalition:lower() == "blue" then coalitionId = coalition.side.BLUE end end if self.enemyCoalitions[coalitionId] then if unit.category then if (unit.category == "plane") or (unit.category == "helicopter" and self.reactOnHelicopters) then veaf.loggers.get(veafQraManager.Id):trace("adding unit to enemy human units for QRA") table.insert(self._enemyHumanUnits, unit.unitName) end end end end end return self._enemyHumanUnits end function VeafQRA:check() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:check()", veaf.p(self.name)) veaf.loggers.get(veafQraManager.Id):debug("self.state=%s", veaf.p(veafQraManager.statusToString(self.state))) veaf.loggers.get(veafQraManager.Id):trace("timer.getTime()=%s", veaf.p(timer.getTime())) --scheduled state application is attempted regardless of airportlink checks etc. to take into account user requested states which go through scheduled_states as well --Stop scheduled is checked before even running the check function as it has the highest priority self:applyScheduledState() if self.state ~= veafQraManager.STATUS_STOP then --if the QRA is linked to an airbase. Airport is checked before even trying to deploy a group and check warehousing which has a lower priority if self.airportLink then veaf.loggers.get(veafQraManager.Id):trace("Checking Airport link : %s", veaf.p(self.airportLink)) self:checkAirport() self:applyScheduledState() end if self.state ~= veafQraManager.STATUS_NOAIRBASE then --if warehousing is activated. Warehousing is checked before even trying to deploy a group if self.QRAcount ~= -1 then veaf.loggers.get(veafQraManager.Id):trace("Checking Warehousing...") veaf.loggers.get(veafQraManager.Id):trace("QRACount : %s", veaf.p(self.QRAcount)) self:checkWarehousing() self:applyScheduledState() end if self.state ~= veafQraManager.STATUS_OUT then local unitNames = self:_getEnemyHumanUnits() local unitsInZone = nil local triggerZone = veaf.getTriggerZone(self.triggerZoneName) if (not veaf.isNullOrEmpty(self.triggerZoneName) and triggerZone == nil) then veaf.loggers.get(veafQraManager.Id):error("QRA has a non-existant zone: " .. self.triggerZoneName) end unitsInZone = {} if triggerZone then if triggerZone.type == 0 then -- circular unitsInZone = mist.getUnitsInZones(unitNames, {self.triggerZoneName}) elseif triggerZone.type == 2 then -- quad point unitsInZone = mist.getUnitsInPolygon(unitNames, triggerZone.verticies) end elseif self.zoneCenter then unitsInZone = veaf.findUnitsInCircle(self.zoneCenter, self.zoneRadius, false, unitNames) else veaf.loggers.get(veafQraManager.Id):error("QRA [%s] has no zone defined, cannot check for units in zone", self.name) return end veaf.loggers.get(veafQraManager.Id):trace("unitsInZone=%s", unitsInZone) local nbUnitsInZone = 0 for _, unit in pairs(unitsInZone) do -- check the unit altitude against the ceiling and floor if unit:inAir() then -- never count a landed aircraft local alt = unit:getPoint().y if alt >= self:getMinimumAltitudeInMeters() and alt <= self:getMaximumAltitudeInMeters() then nbUnitsInZone = nbUnitsInZone + 1 end end end veaf.loggers.get(veafQraManager.Id):trace("nbUnitsInZone=%s", nbUnitsInZone) if (self.state == veafQraManager.STATUS_READY) and (unitsInZone and nbUnitsInZone > 0) then veaf.loggers.get(veafQraManager.Id):debug("self.state set to veafQraManager.STATUS_READY_WAITINGFORMORE at timer.getTime()=%s", timer.getTime()) self.state = veafQraManager.STATUS_READY_WAITINGFORMORE self.timeSinceReady = timer.getTime() elseif (self.state == veafQraManager.STATUS_READY_WAITINGFORMORE) and (unitsInZone and nbUnitsInZone > 0) and (timer.getTime() - self.timeSinceReady > self.delayBeforeActivating) then -- trigger the QRA self:deploy(nbUnitsInZone) self.timeSinceReady = -1 elseif (self.state == veafQraManager.STATUS_DEAD) and (self.noNeedToLeaveZoneBeforeRearming or (not unitsInZone or nbUnitsInZone == 0)) then -- rearm the QRA after a delay (if set) if self.delayBeforeRearming > 0 then mist.scheduleFunction(VeafQRA.rearm, {self}, timer.getTime()+self.delayBeforeRearming) self.state = veafQraManager.STATUS_WILLREARM else self:rearm() end elseif (self.state == veafQraManager.STATUS_ACTIVE) then local qraAlive = false local qraInAir = false for _, groupName in pairs(self.spawnedGroupsNames) do local group = Group.getByName(groupName) if group then local groupAtLeastOneUnitAlive = false local groupAtLeastOneUnitInAir = false local category = group:getCategory() local units = group:getUnits() if units then for _,unit in pairs(units) do if unit then local unitLife = unit:getLife() local unitLife0 = 0 if unit.getLife0 then -- statics have no life0 unitLife0 = unit:getLife0() end local unitLifePercent = unitLife if unitLife0 > 0 then unitLifePercent = 100 * unitLife / unitLife0 end if unitLifePercent >= veafQraManager.MINIMUM_LIFE_FOR_QRA_IN_PERCENT then groupAtLeastOneUnitAlive = true end if category == 0 --[[airplanes]] or category == 1 --[[helicopters]] then -- check if at least one unit is still airborne if unit:inAir() then groupAtLeastOneUnitInAir = true end else -- consider that ground units have never landed groupAtLeastOneUnitInAir = true end end end end qraAlive = qraAlive or groupAtLeastOneUnitAlive qraInAir = qraInAir or groupAtLeastOneUnitInAir veaf.loggers.get(veafQraManager.Id):trace("qraAlive=%s", veaf.p(qraAlive)) veaf.loggers.get(veafQraManager.Id):trace("qraInAir=%s", veaf.p(qraInAir)) end end if not qraAlive then -- signal QRA destroyed self:destroyed() elseif (self.resetWhenLeavingZone and nbUnitsInZone == 0) or not qraInAir then -- QRA reset self:rearm() end end end end mist.scheduleFunction(VeafQRA.check, {self}, timer.getTime() + veafQraManager.WATCHDOG_DELAY) end end function VeafQRA:setScheduledState(scheduledState) --priority level 1 veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setScheduledState(%s)", veaf.p(self.name), veaf.p(scheduledState)) if scheduledState == veafQraManager.STATUS_STOP then self.scheduled_state = veafQraManager.STATUS_STOP veaf.loggers.get(veafQraManager.Id):debug("QRA STOP scheduled") --priority level 2 elseif scheduledState == veafQraManager.STATUS_NOAIRBASE and self.scheduled_state ~= veafQraManager.STATUS_STOP then self.scheduled_state = veafQraManager.STATUS_NOAIRBASE veaf.loggers.get(veafQraManager.Id):debug("QRA NOAIRBASE scheduled") --priority level 3 elseif scheduledState == veafQraManager.STATUS_OUT and self.scheduled_state ~= veafQraManager.STATUS_STOP and self.scheduled_state ~= veafQraManager.STATUS_NOAIRBASE then self.scheduled_state = veafQraManager.STATUS_OUT veaf.loggers.get(veafQraManager.Id):debug("QRA OUT scheduled") end return self end function VeafQRA:applyScheduledState() if self.scheduled_state and self.state ~= veafQraManager.STATUS_ACTIVE then veaf.loggers.get(veafQraManager.Id):debug("QRA taking scheduled status : %s", veaf.p(self.scheduled_state)) self.state = self.scheduled_state end end function VeafQRA:checkAirport() local QRA_airportObject = veaf.getAirbaseForCoalition(self.airportLink, self.coalition) local airport_life_percent = nil if QRA_airportObject then airport_life_percent = veaf.getAirbaseLife(self.airportLink, true) end veaf.loggers.get(veafQraManager.Id):trace("VeafQRA[%s] is linked to airbase %s", veaf.p(self.name), veaf.p(self.airportLink)) if not QRA_airportObject or airport_life_percent < self.airportMinLifePercent then veaf.loggers.get(veafQraManager.Id):trace("QRA lost it's airbase") self:setScheduledState(veafQraManager.STATUS_NOAIRBASE) if not self.silent and not self.noAB_announced then local msg = string.format(self.messageAirbaseDown, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onAirbaseDown then self.onAirbaseDown(QRA_airportObject) end self.noAB_announced = true elseif self.state == veafQraManager.STATUS_NOAIRBASE then veaf.loggers.get(veafQraManager.Id):trace("QRA has it's airbase %s", veaf.p(QRA_airportObject:getName())) if not self.silent then local msg = string.format(self.messageAirbaseUp, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onAirbaseUp then self.onAirbaseUp(QRA_airportObject) end self.noAB_announced = false self.state = veafQraManager.STATUS_DEAD --QRA that have just been recommisionned act as if they were dead since they need to be rearmed after a delay if self.scheduled_state == veafQraManager.STATUS_NOAIRBASE then self.scheduled_state = nil end --make sure you reset the scheduled state if you are within the bounds of this method end end -- -- maximum number of QRA ready for action at once, -1 indicates infinite -- QRAmaxCount = -1 -- -- number of groups of aircrafts that can be spawned for this QRA in total, -1 indicates infinite -- QRAcount = -1 -- -- delay in minutes before the QRA counter is increased by one, simulating some sort of logistic chain of aircrafts. -- delayBeforeQRAresupply = 0 -- -- maximum number of resupplies at a given time, simulating some sort of warehousing, -1 indicates infinite. Is decremented every time a resupply happens if not equal to -1 originally. 0 indicated no resupply. -- QRAresupplyMax = -1 -- -- minimum QRAcount that will trigger a resupply, -1 indicates as soon as an aircraft is lost -- QRAminCountforResupply = -1 -- -- how many aircraft groups are resupplied at once --resupplyAmount = 1 -- -- indicator to know if the QRA is being resupplied or not --isResupplying = false function VeafQRA:checkWarehousing() veaf.loggers.get(veafQraManager.Id):trace("VeafQRA[%s] resupply state is %s", veaf.p(self.name), veaf.p(self.isResupplying)) --if a resupply is not already on the way and if there are aircrafts in stock and if the available aircraft count is below the threshold or if an aircraft was just lost and the resupply mode indicates to resupply whenever an aircraft is lost if not self.isResupplying and self.QRAresupplyMax ~= 0 and (self.QRAcount < self.QRAminCountforResupply or (self.QRAcount < self.QRAmaxCount or (self.QRAmaxCount == -1 and self.state == veafQraManager.STATUS_DEAD)) and self.QRAminCountforResupply == -1) then veaf.loggers.get(veafQraManager.Id):trace("QRA has %s/%s aircraft groups available", veaf.p(self.QRAcount), veaf.p(self.QRAmaxCount)) veaf.loggers.get(veafQraManager.Id):trace("QRA has %s aircraft groups ready for resupply (-1 for infinite)", veaf.p(self.QRAresupplyMax)) veaf.loggers.get(veafQraManager.Id):trace("QRA resupply asks for %s aircraft groups", veaf.p(self.resupplyAmount)) local resupplyAmount = self.resupplyAmount --take into account the maximum number of QRA groups as to not oversupply it if self.QRAmaxCount ~= -1 and resupplyAmount > self.QRAmaxCount - self.QRAcount then resupplyAmount = self.QRAmaxCount - self.QRAcount veaf.loggers.get(veafQraManager.Id):trace("There are only %s available aircraft group slots for this QRA", veaf.p(self.QRAmaxCount - self.QRAcount)) end --take into account the maximum number of QRA groups that can be supplied by the stock if self.QRAresupplyMax ~= -1 and resupplyAmount > self.QRAresupplyMax then resupplyAmount = self.QRAresupplyMax veaf.loggers.get(veafQraManager.Id):trace("QRA can only be resupplied by %s aircraft groups", veaf.p(self.QRAresupplyMax)) end veaf.loggers.get(veafQraManager.Id):trace("%s aircraft groups will be handled for resupply", veaf.p(resupplyAmount)) if resupplyAmount > 0 then self.isResupplying = true if self.delayBeforeQRAresupply > 0 then veaf.loggers.get(veafQraManager.Id):trace("QRA will be resupplied in %s seconds", veaf.p(self.delayBeforeQRAresupply)) mist.scheduleFunction(VeafQRA.resupply, {self, resupplyAmount}, timer.getTime()+self.delayBeforeQRAresupply) else veaf.loggers.get(veafQraManager.Id):trace("QRA is being resupplied...") self:resupply(resupplyAmount) end end end if self.QRAcount == 0 then veaf.loggers.get(veafQraManager.Id):trace("QRA is out of aircraft groups") if not self.silent and not self.outAnnounced then local msg = string.format(self.messageOut, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end self.outAnnounced = true end if self.onOut then self.onOut() end self:setScheduledState(veafQraManager.STATUS_OUT) end end function VeafQRA:resupply(resupplyAmount) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:resupply(%s)", veaf.p(self.name), veaf.p(resupplyAmount)) --if the QRA can still operate, then execute the resupply, the list would need to be expanded if new scheduled status blocking operations were added if self.scheduled_state ~= veafQraManager.STATUS_NOAIRBASE and self.scheduled_state ~= veafQraManager.STATUS_STOP then if resupplyAmount and type(resupplyAmount) == 'number' and resupplyAmount > 0 then veaf.loggers.get(veafQraManager.Id):trace("QRA is going to be resupplied, old count is : %s", veaf.p(self.QRAcount)) self.QRAcount = self.QRAcount + resupplyAmount veaf.loggers.get(veafQraManager.Id):trace("QRA was resupplied, new count is : %s", veaf.p(self.QRAcount)) veaf.loggers.get(veafQraManager.Id):trace("QRA previously had %s aircraft groups ready for resupply (-1 for infinite)", veaf.p(self.QRAresupplyMax)) if self.QRAresupplyMax ~= -1 then self.QRAresupplyMax = self.QRAresupplyMax - resupplyAmount if self.QRAresupplyMax < 0 then self.QRAresupplyMax = 0 end end veaf.loggers.get(veafQraManager.Id):trace("QRA now only has %s aircraft groups ready for resupply (-1 for infinite)", veaf.p(self.QRAresupplyMax)) if self.state == veafQraManager.STATUS_OUT then veaf.loggers.get(veafQraManager.Id):trace("QRA now has at least one aircraft group ready for action, resuming service...") if not self.silent then local msg = string.format(self.messageResupplied, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onResupplied then self.onResupplied() end self.outAnnounced = false self.state = veafQraManager.STATUS_DEAD --QRA that have just arrived act as if the QRA had just died, they need to be rearmed if self.scheduled_state == veafQraManager.STATUS_OUT then self.scheduled_state = nil end --make sure you reset the scheduled state if you are within the bounds of this method end end else veaf.loggers.get(veafQraManager.Id):trace("QRA is no longer operating, resupply did not take place") end self.isResupplying = false end function VeafQRA:chooseGroupsToDeploy(nbUnitsInZone) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:chooseGroupsToDeploy(%s)", veaf.p(self.name), veaf.p(nbUnitsInZone)) local biggestNumberLowerThanUnitsInZone = -1 local groupsToDeploy = nil for enemyNb, groups in pairs(self.groupsToDeployByEnemyQuantity) do if nbUnitsInZone >= enemyNb then biggestNumberLowerThanUnitsInZone = enemyNb groupsToDeploy = groups end end if groupsToDeploy then -- process a random group definition local groupsToChooseFrom = groupsToDeploy[1] local numberOfGroups = groupsToDeploy[2] local bias = groupsToDeploy[3] if groupsToChooseFrom and type(groupsToChooseFrom) == "table" and numberOfGroups and type(numberOfGroups) == "number" and bias and type(bias) == "number" then local result = {} for _ = 1, numberOfGroups do local group = veaf.randomlyChooseFrom(groupsToChooseFrom, bias) veaf.loggers.get(veafQraManager.Id):trace("group=%s", veaf.p(group)) table.insert(result, group) end groupsToDeploy = result end end return groupsToDeploy end function VeafQRA:deploy(nbUnitsInZone) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:deploy()", veaf.p(self.name)) veaf.loggers.get(veafQraManager.Id):trace("nbUnitsInZone=[%s]", veaf.p(nbUnitsInZone)) if self.minimumNbEnemyPlanes ~= -1 and self.minimumNbEnemyPlanes > nbUnitsInZone then veaf.loggers.get(veafQraManager.Id):trace("not enough enemies in zone, min=%s", veaf.p(self.minimumNbEnemyPlanes)) return end if not self.silent then local msg = string.format(self.messageDeploy, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end local groupsToDeploy = self:chooseGroupsToDeploy(nbUnitsInZone) self.spawnedGroupsNames = {} if groupsToDeploy then local zoneCenter = {} if self.triggerZoneName then local triggerZone = veaf.getTriggerZone(self.triggerZoneName) zoneCenter.x = triggerZone.x zoneCenter.z = triggerZone.y zoneCenter.y = 0 elseif self.zoneCenter then zoneCenter = self.zoneCenter end for _, groupNameOrCommand in pairs(groupsToDeploy) do -- check if this is a DCS group or a VEAF command if veaf.startsWith(groupNameOrCommand, "[") or veaf.startsWith(groupNameOrCommand, "-") then -- this is a command local command = groupNameOrCommand local latDelta = self.respawnDefaultOffset.latDelta local lonDelta = self.respawnDefaultOffset.lonDelta if veaf.startsWith(groupNameOrCommand, "[") then -- extract relative coordinates and the actual command local coords coords, command = groupNameOrCommand:match("%[(.*)%](.*)") veaf.loggers.get(veafQraManager.Id):trace("coords=%s", veaf.p(coords)) veaf.loggers.get(veafQraManager.Id):trace("command=%s", veaf.p(command)) if coords then latDelta, lonDelta = coords:match("([%+-%d]+),%s*([%+-%d]+)") end end veaf.loggers.get(veafQraManager.Id):debug("running command [%s]", veaf.p(command)) veaf.loggers.get(veafQraManager.Id):trace("latDelta = [%s]", veaf.p(latDelta)) veaf.loggers.get(veafQraManager.Id):trace("lonDelta = [%s]", veaf.p(lonDelta)) local position = {x = zoneCenter.x - lonDelta, y = zoneCenter.y, z = zoneCenter.z + latDelta} local randomPosition = mist.getRandPointInCircle(position, self.respawnRadius) local spawnedGroupsNames = {} veafInterpreter.execute(command, randomPosition, self.coalition, nil, spawnedGroupsNames) for _, newGroupName in pairs(spawnedGroupsNames) do table.insert(self.spawnedGroupsNames, newGroupName) end else -- this is a DCS group local groupName = groupNameOrCommand veaf.loggers.get(veafQraManager.Id):debug("spawning group [%s]", veaf.p(groupName)) local group = Group.getByName(groupName) if not group then veaf.loggers.get(veafQraManager.Id):error("group [%s] does not exist in the mission!", veaf.p(groupName)) else veaf.loggers.get(veafQraManager.Id):debug("group=%s", veaf.p(group)) veaf.loggers.get(veafQraManager.Id):debug("group:getUnits()=%s", veaf.p(group:getUnits())) local spawnSpot = {x = zoneCenter.x - self.respawnDefaultOffset.lonDelta, y = zoneCenter.y, z = zoneCenter.z + self.respawnDefaultOffset.latDelta} -- Try and set the spawn spot at the place the group has been set in the Mission Editor. -- Unfortunately this is sometimes not possible because DCS is not returning the group units for some reason. -- When this happens we'll default to the default spawn offset (same as spawning with VEAF commands) if not group:getUnit(1) then veaf.loggers.get(veafQraManager.Id):warn("group [%s] does not have any unit!", veaf.p(groupName)) else spawnSpot = group:getUnit(1):getPoint() end local vars = {} vars.point = mist.getRandPointInCircle(spawnSpot, self.respawnRadius) vars.point.z = vars.point.y vars.point.y = spawnSpot.y vars.gpName = groupName vars.action = 'clone' vars.route = mist.getGroupRoute(groupName, 'task') local newGroup = mist.teleportToPoint(vars) -- respawn with radius if newGroup then table.insert(self.spawnedGroupsNames, newGroup.name) end end end end veaf.loggers.get(veafQraManager.Id):trace("self.spawnedGroups=%s", veaf.p(self.spawnedGroupsNames)) self.state = veafQraManager.STATUS_ACTIVE end if self.onDeploy then self.onDeploy(nbUnitsInZone) end end function VeafQRA:destroyed() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:destroyed()", veaf.p(self.name)) if not self.silent then local msg = string.format(self.messageDestroyed, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onDestroyed then self.onDestroyed() end self.state = veafQraManager.STATUS_DEAD if self.QRAcount > 0 then veaf.loggers.get(veafQraManager.Id):trace("QRA will now see one of it's aicraft groups removed") self.QRAcount = self.QRAcount - 1 end end function VeafQRA:rearm(silent) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:rearm()", veaf.p(self.name)) if not self.silent and not silent then local msg = string.format(self.messageReady, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.spawnedGroupsNames then for _, groupName in pairs(self.spawnedGroupsNames) do local group = Group.getByName(groupName) if group then group:destroy() end end end if self.onReady then self.onReady() end self.state = veafQraManager.STATUS_READY end function VeafQRA:start() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:start()", veaf.p(self.name)) self.scheduled_state = nil --make sure you reset the scheduled state if you are within the bounds of this method self:rearm() self:check() -- draw the zone if self.drawZone then if self.triggerZoneName then self.zoneDrawing = mist.marker.drawZone(self.triggerZoneName, {message=self:getDescription(), readOnly=true}) else self.zoneDrawing = VeafCircleOnMap:new() :setName(self:getName()) :setCoalition(self:getEnnemyCoalition()) :setCenter(self.zoneCenter) :setRadius(self.zoneRadius) :setLineType("dashed") :setColor("white") :setFillColor("transparent") :draw() end end if not self.silent then local msg = string.format(self.messageStart, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onStart then self.onStart() end return self end function VeafQRA:stop(silent) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:stop()", veaf.p(self.name)) self:setScheduledState(veafQraManager.STATUS_STOP) -- just in case, despawn the spawned groups if self.spawnedGroupsNames then for _, groupName in pairs(self.spawnedGroupsNames) do local group = Group.getByName(groupName) if group then group:destroy() end end end -- erase the zone if self.zoneDrawing then if self.triggerZoneName then mist.marker.remove(self.zoneDrawing.markId) else self.zoneDrawing:erase() end self.zoneDrawing = nil end if not self.silent and not silent then local msg = string.format(self.messageStop, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onStop then self.onStop() end return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafQraManager.add(aQraObject, aName) local name = aName or aQraObject:getName() veafQraManager.qras[name] = aQraObject return aQraObject end function veafQraManager.get(aNameString) return veafQraManager.qras[aNameString] end --- --- called from veafEventHandler when a unit is created function veafQraManager.eventHandler(event) -- find the originator unit local unitName = event.initiator and event.initiator.unitName if not unitName then return end if mist.DBs.humansByName[unitName] then -- it's a human unit local unit = event.initiator if unit ~= nil then -- handle the event on all QRAs for _, qra in pairs(veafQraManager.qras) do qra:humanBornEvent(unit) end end end end function veafQraManager.initialize() veaf.loggers.get(veafQraManager.Id):debug("veafQraManager.initialize()") veafEventHandler.addCallback("veafQraManager.eventHandler", {"S_EVENT_BIRTH", "S_EVENT_PLAYER_ENTER_UNIT"}, veafQraManager.eventHandler) end veaf.loggers.get(veafQraManager.Id):info("Loading version %s", veafQraManager.Version) ------------------ END script veafQraManager.lua ------------------ ------------------ START script veafSanctuary.lua ------------------ ------------------------------------------------------------------ -- VEAF Sanctuary Zone script -- By zip (2021) -- -- Features: -- --------- -- * This module offers support for creating sanctuary zones in a mission -- * A sanctuary zone warns and then destroys all the human aircrafts of other coalitions when they loiter in the zone -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafSanctuary = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafSanctuary.Id = "SANCTUARY" --- Version. veafSanctuary.Version = "1.6.2" -- trace level, specific to this module --veafSanctuary.LogLevel = "trace" veaf.loggers.new(veafSanctuary.Id, veafSanctuary.LogLevel) veafSanctuary.RecordAction = true veafSanctuary.RecordTrace = false veafSanctuary.RecordTraceTrespassing = false veafSanctuary.RecordTraceShooting = false -- delay before the sanctuary zones start reporting veafSanctuary.DelayForStartup = 0 -- delay between each check of the sanctuary zones veafSanctuary.DelayBetweenChecks = 15 -- default delay before warning veafSanctuary.DEFAULT_DELAY_WARNING = 0 -- default message when entering the zone veafSanctuary.DEFAULT_MESSAGE_WARNING = "Warning, %s : you've entered a sanctuary zone and will be shot in %d seconds if you don't leave IMMEDIATELY" -- time to display the messages veafSanctuary.MESSAGE_TIME = 20 -- default delay before instantly killing the offender veafSanctuary.DEFAULT_DELAY_INSTANT = -1 -- default delay before spawning defenses veafSanctuary.DEFAULT_DELAY_SPAWN = -1 -- default message when defenses are spawned veafSanctuary.DEFAULT_MESSAGE_SPAWN = "You've been warned : deploying defense systems" -- time to start spawning harder defenses veafSanctuary.HARDER_DEFENSES_AFTER = 75 -- time to start removing the defenses veafSanctuary.DELETE_DEFENSES_AFTER = 75 -- time before handling weapons veafSanctuary.DESTROY_WEAPONS_AFTER = 2 -- clean slate veafSanctuary.FORGIVE_SHOOTER_AFTER = 10 * 60 -- 10 minutes -- default message to target when weapon launch is detected veafSanctuary.DEFAULT_MESSAGE_SHOT_TARGET = "Warning, %s : you've been attacked by %s ; we destroyed the missile in the air !" -- default message to launcher when weapon launch is detected veafSanctuary.DEFAULT_MESSAGE_SHOT_LAUNCHER = "Warning, %s : you've attacked %s ; we destroyed the missile in the air. Don't do that again or we'll destroy you !" -- number of offenses (misile launches at players in a zone) that will justify destruction veafSanctuary.DEFAULT_OFFENSES_BEFORE_DESTRUCTION = 3 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafSanctuary.initialized = false veafSanctuary.spawnedSAMs = {} veafSanctuary.humanUnitsToFollow = {} veafSanctuary.zonesList = {} veafSanctuary.humanUnits = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafSanctuary._recordAction(message) if message and veafSanctuary.RecordAction then local _filename = "sanctuary_zones" if veaf.config.MISSION_NAME then veaf.loggers.get(veafSanctuary.Id):trace(string.format("veaf.config.MISSION_NAME=%s", veaf.p(veaf.config.MISSION_NAME))) _filename = _filename .. "-" .. veaf.config.MISSION_NAME end if veaf.config.SERVER_NAME then veaf.loggers.get(veafSanctuary.Id):trace(string.format("veaf.config.SERVER_NAME=%s", veaf.p(veaf.config.SERVER_NAME))) _filename = _filename .. "-" .. veaf.config.SERVER_NAME end _filename = _filename .. ".log" veaf.loggers.get(veafSanctuary.Id):trace(string.format("_filename=%s", veaf.p(_filename))) veaf.writeLineToTextFile(message, _filename) end end function veafSanctuary.recordAction(message) if message then local _message = "ACTION - " .. message veaf.loggers.get(veafSanctuary.Id):info(_message) veafSanctuary._recordAction(veafSanctuary._recordAction(" INFO SCRIPTING: VEAF - I - " .. _message)) end end function veafSanctuary.recordTrace(message) if message and veafSanctuary.RecordTrace then local _message = "SANCTUARY - " .. message veaf.loggers.get(veafSanctuary.Id):trace(_message) veafSanctuary._recordAction(" INFO SCRIPTING: VEAF - T - " .. _message) end end function veafSanctuary.recordTraceShooting(message) if message and veafSanctuary.RecordTraceShooting then local _message = "SHOOTING - " .. message veaf.loggers.get(veafSanctuary.Id):trace(_message) veafSanctuary._recordAction(" INFO SCRIPTING: VEAF - T - " .. _message) end end function veafSanctuary.recordTraceTrespassing(message) if message and veafSanctuary.RecordTraceTrespassing then local _message = "TRESPASS - " .. message veaf.loggers.get(veafSanctuary.Id):trace(_message) veafSanctuary._recordAction(" INFO SCRIPTING: VEAF - T - " .. _message) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- objects ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafSanctuaryZone = {} function VeafSanctuaryZone:new(objectToCopy) local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object -- name objectToCreate.name = nil -- coalition that is forbidden to enter the zone objectToCreate.protectFromCoalition = nil -- if true, missiles fired at units in the zone will be destroyed objectToCreate.protectFromMissiles = nil -- position on the map objectToCreate.position = nil -- if set, the zone is a circle of center *position* and of radius *radius* objectToCreate.radius = nil objectToCreate.radiusSquared = nil -- if set, the zone is a polygon - this is a simple list of points objectToCreate.polygon = nil -- delay before warning - if -1, no warning objectToCreate.delayWarning = veafSanctuary.DEFAULT_DELAY_WARNING -- warning message objectToCreate.messageWarning = veafSanctuary.DEFAULT_MESSAGE_WARNING -- delay before instant kill - if -1, no instant kill objectToCreate.delayInstant = veafSanctuary.DEFAULT_DELAY_INSTANT -- delay before spawn of defense systems - if -1, no spawn objectToCreate.delaySpawn = veafSanctuary.DEFAULT_DELAY_SPAWN -- spawn message objectToCreate.messageSpawn = veafSanctuary.DEFAULT_MESSAGE_SPAWN -- message to target when weapon launch is detected objectToCreate.messageShotTarget = veafSanctuary.DEFAULT_MESSAGE_SHOT_TARGET --message to launcher when weapon launch is detected objectToCreate.messageShotLauncher = veafSanctuary.DEFAULT_MESSAGE_SHOT_LAUNCHER objectToCreate.spawnedGroups = {} objectToCreate.offensesByOffender = {} objectToCreate.offensesBeforeDestruction = veafSanctuary.DEFAULT_OFFENSES_BEFORE_DESTRUCTION return objectToCreate end --- --- setters and getters --- function VeafSanctuaryZone:setName(value) self.name = value return self end function VeafSanctuaryZone:getName() return self.name end function VeafSanctuaryZone:setCoalition(value) veaf.loggers.get(veafSanctuary.Id):debug(string.format("VeafSanctuaryZone[%s]:setCoalition(%s)", veaf.p(self.name), veaf.p(value))) self.coalition = value return self end function VeafSanctuaryZone:getCoalition() return self.coalition end function VeafSanctuaryZone:setProtectFromMissiles() veaf.loggers.get(veafSanctuary.Id):trace(string.format("VeafSanctuaryZone[%s]:setProtectFromMissiles()", veaf.p(self.name))) self.protectFromMissiles = true return self end function VeafSanctuaryZone:isProtectFromMissiles() return self.protectFromMissiles end function VeafSanctuaryZone:setPosition(value) self.position = value return self end function VeafSanctuaryZone:getPosition() return self.position end function VeafSanctuaryZone:setRadius(value) self.radius = value if value ~= nil then self.radiusSquared = value * value else self.radiusSquared = nil end return self end function VeafSanctuaryZone:getRadius() return self.radius end function VeafSanctuaryZone:setPolygon(value) self.polygon = value return self end function VeafSanctuaryZone:setPolygonFromUnitsInSequence(unitNamePrefix, markPositions) veaf.loggers.get(veafSanctuary.Id):trace(string.format("VeafSanctuaryZone[%s]:setPolygonFromUnitsInSequence(%s, %s)", veaf.p(self.name), veaf.p(unitNamePrefix), veaf.p(markPositions))) local unitNames = {} local sequence = 0 while true do sequence = sequence + 1 local unitName = string.format("%s #%03d", unitNamePrefix, sequence) --veaf.loggers.get(veafSanctuary.Id):trace(string.format("unitName=%s", veaf.p(unitName))) local unit = Unit.getByName(unitName) if not unit then local group = Group.getByName(unitName) if group then unit = group:getUnit(1) end end --veaf.loggers.get(veafSanctuary.Id):trace(string.format("unit=%s", veaf.p(veaf.ifnn(unit, "getID")))) if not unit then return self:setPolygonFromUnits(unitNames, markPositions) else table.insert(unitNames, unitName) end end end function VeafSanctuaryZone:setPolygonFromUnits(unitNames, markPositions) -- Color of the line marking the zone ({r, g, b, a}) local LINE_COLOR = {0/255, 255/255, 100/255, 255/255} local LINE_TYPE = VeafDrawingOnMap.LINE_TYPE.twodashes veaf.loggers.get(veafSanctuary.Id):debug(string.format("VeafSanctuaryZone[%s]:setPolygonFromUnits()", veaf.p(self.name))) veaf.loggers.get(veafSanctuary.Id):trace(string.format("markPositions = %s", veaf.p(markPositions))) local polygon = veaf.getPolygonFromUnits(unitNames) if polygon and #polygon > 0 then veaf.loggers.get(veafSanctuary.Id):trace(string.format("polygon = %s", veaf.p(polygon))) self:setPolygon(polygon) if markPositions then local drawing = VeafDrawingOnMap:new() :setName(self:getName()) :setColor(LINE_COLOR) :setLineType(LINE_TYPE) :addPoints(self:getPolygon()) :setCoalition(self:getCoalition()) :draw() end end return self end function VeafSanctuaryZone:getPolygon() return self.polygon end function VeafSanctuaryZone:setDelayWarning(value) self.delayWarning = value return self end function VeafSanctuaryZone:getDelayWarning() return self.delayWarning end function VeafSanctuaryZone:setOffensesBeforeDestruction(value) self.offensesBeforeDestruction = value return self end function VeafSanctuaryZone:getOffensesBeforeDestruction() return self.offensesBeforeDestruction end function VeafSanctuaryZone:setMessageWarning(value) self.messageWarning = value return self end function VeafSanctuaryZone:getMessageWarning() return self.messageWarning end function VeafSanctuaryZone:setMessageShotTarget(value) self.messageShotTarget = value return self end function VeafSanctuaryZone:getMessageShotTarget() return self.messageShotTarget end function VeafSanctuaryZone:setMessageShotLauncher(value) self.messageShotLauncher = value return self end function VeafSanctuaryZone:getMessageShotLauncher() return self.messageShotLauncher end function VeafSanctuaryZone:setDelayInstant(value) self.delayInstant = value return self end function VeafSanctuaryZone:getDelayInstant() return self.delayInstant end function VeafSanctuaryZone:setDelaySpawn(value) self.delaySpawn = value return self end function VeafSanctuaryZone:getDelaySpawn() return self.delaySpawn end function VeafSanctuaryZone:setMessageSpawn(value) self.messageSpawn = value return self end function VeafSanctuaryZone:getMessageSpawn() return self.messageSpawn end function VeafSanctuaryZone:addSpawnedGroups(spawnedGroupsNames) for _, groupName in pairs(spawnedGroupsNames) do self.spawnedGroups[groupName] = timer.getTime() end return self end function VeafSanctuaryZone:getSpawnedGroups() return self.spawnedGroups end --- --- business methods --- function VeafSanctuaryZone:deployDefenses(position, unit, timeInZone) veaf.loggers.get(veafSanctuary.Id):trace(string.format("VeafSanctuaryZone[%s]:deployDefenses()", veaf.p(self.name))) veaf.loggers.get(veafSanctuary.Id):trace(string.format("position=%s", veaf.p(position))) -- compute the position of the unit in 20 seconds local positionIn20s = mist.vec.add(position, mist.vec.scalarMult(unit:getVelocity(), 20)) veaf.loggers.get(veafSanctuary.Id):trace(string.format("positionIn20s=%s", veaf.p(positionIn20s))) -- compute the position of the unit in 40 seconds, local positionIn40s = mist.vec.add(position, mist.vec.scalarMult(unit:getVelocity(), 40)) veaf.loggers.get(veafSanctuary.Id):trace(string.format("positionIn40s=%s", veaf.p(positionIn40s))) -- compute a heading towards the unit local heading = mist.utils.round(mist.utils.toDegree(mist.getHeading(unit)), 0) veaf.loggers.get(veafSanctuary.Id):trace(string.format("heading=%s", veaf.p(heading))) local heading1 = heading*math.random(70,130)/100 veaf.loggers.get(veafSanctuary.Id):trace(string.format("heading1=%s", veaf.p(heading1))) local heading1S = string.format(", hdg %s", tostring(veaf.invertHeading(heading1))) veaf.loggers.get(veafSanctuary.Id):trace(string.format("heading1S=%s", veaf.p(heading1S))) local heading2 = heading*math.random(70,130)/100 veaf.loggers.get(veafSanctuary.Id):trace(string.format("heading2=%s", veaf.p(heading2))) local heading2S = string.format(", hdg %s", tostring(veaf.invertHeading(heading2))) veaf.loggers.get(veafSanctuary.Id):trace(string.format("heading2S=%s", veaf.p(heading2S))) if veafShortcuts then local ship1 = "-burke" local ship2 = "-ticonderoga" local sam1 = "-roland" local sam2 = "-patriot" if self:getCoalition() == 1 then -- red side units ship1 = "-rezky" ship2 = "-pyotr" sam1 = "-roland" sam2 = "-patriot" end local spawnedGroupsNames = {} local surfaceType = land.getSurfaceType(mist.utils.makeVec2(position)) veaf.loggers.get(veafSanctuary.Id):trace(string.format("surfaceType=%s", veaf.p(surfaceType))) if surfaceType == 2 or surfaceType == 3 then -- this is water veafShortcuts.ExecuteAlias(ship1, "radius 2000, multiplier 2, skynet false"..heading1S, positionIn20s, self:getCoalition(), nil, true, spawnedGroupsNames) veafShortcuts.ExecuteAlias(ship1, "radius 3000, multiplier 2, skynet false"..heading2S, positionIn40s, self:getCoalition(), nil, true, spawnedGroupsNames) else -- this is land veafShortcuts.ExecuteAlias(sam1, "radius 2000, multiplier 2, skynet false"..heading1S, positionIn20s, self:getCoalition(), nil, true, spawnedGroupsNames) veafShortcuts.ExecuteAlias(sam1, "radius 2000, multiplier 2, skynet false"..heading2S, positionIn20s, self:getCoalition(), nil, true, spawnedGroupsNames) end self:addSpawnedGroups(spawnedGroupsNames) veaf.loggers.get(veafSanctuary.Id):trace(string.format("spawnedGroupsNames = %s", veaf.p(spawnedGroupsNames))) if timeInZone > veafSanctuary.HARDER_DEFENSES_AFTER then if surfaceType == 2 or surfaceType == 3 then -- this is water veafShortcuts.ExecuteAlias(ship2, "radius 3000, multiplier 2, skynet false"..heading1S, positionIn20s, self:getCoalition(), nil, true, spawnedGroupsNames) veafShortcuts.ExecuteAlias(ship2, "radius 4000, multiplier 2, skynet false"..heading2S, positionIn40s, self:getCoalition(), nil, true, spawnedGroupsNames) else -- this is land veafShortcuts.ExecuteAlias(sam2, "radius 3000, skynet false"..heading1S, positionIn20s, self:getCoalition(), nil, true, spawnedGroupsNames) veafShortcuts.ExecuteAlias(sam2, "radius 4000, skynet false"..heading2S, positionIn40s, self:getCoalition(), nil, true, spawnedGroupsNames) end self:addSpawnedGroups(spawnedGroupsNames) veaf.loggers.get(veafSanctuary.Id):trace(string.format("spawnedGroupsNames = %s", veaf.p(spawnedGroupsNames))) end end end function VeafSanctuaryZone:cleanupDefenses() veaf.loggers.get(veafSanctuary.Id):trace(string.format("VeafSanctuaryZone[%s]:cleanupDefenses()", veaf.p(self.name))) local oldestTimeToKeep = timer.getTime() - veafSanctuary.DELETE_DEFENSES_AFTER for name, time in pairs(self:getSpawnedGroups()) do if time < oldestTimeToKeep then local group = Group.getByName(name) if group then group:destroy() self:getSpawnedGroups()[name] = nil end end end end function VeafSanctuaryZone:isPositionInZone(position) local inZone = false if self:getPolygon() then veaf.loggers.get(veafSanctuary.Id):trace("polygon mode") inZone = mist.pointInPolygon(position, self:getPolygon()) elseif self:getPosition() then veaf.loggers.get(veafSanctuary.Id):trace("circle and radius mode") local distanceFromCenter = ((position.x - self:getPosition().x)^2 + (position.z - self:getPosition().z)^2)^0.5 veaf.loggers.get(veafSanctuary.Id):trace(string.format("distanceFromCenter=%d, radius=%d", distanceFromCenter, self:getRadius())) inZone = distanceFromCenter < self:getRadius() end return inZone end function VeafSanctuaryZone:forgive(playerName) veaf.loggers.get(veafSanctuary.Id):trace(string.format("VeafSanctuaryZone[%s]:forgive(%s)", veaf.p(self.name), veaf.p(playerName))) self.offensesByOffender[playerName] = 0 end function VeafSanctuaryZone:handleWeapon(weapon) veafSanctuary.recordTraceShooting(string.format("VeafSanctuaryZone[%s]:handleWeapon()", veaf.p(self.name))) veafSanctuary.recordTraceShooting(string.format("weapon=%s", veaf.p(veaf.ifnns(weapon, {"getID", "getName", "getTypeName"})))) if not weapon then return end if self:isProtectFromMissiles() then -- check if the missile was shot by a human from the other coalition local launcherUnit = weapon:getLauncher() veafSanctuary.recordTraceShooting(string.format("launcherUnit=%s", veaf.p(veaf.ifnns(launcherUnit, {"getID", "getName", "getTypeName", "getPlayerName", "getCoalition"})))) if launcherUnit and launcherUnit:getCoalition() ~= self:getCoalition() then local launcherPlayername = launcherUnit:getPlayerName() -- TODO debug - REMOVE LATER -- if not launcherPlayername or launcherPlayername == "" then -- launcherPlayername = "AI-launcher" -- end -- TODO debug - REMOVE LATER if launcherPlayername and launcherPlayername ~= "" then -- check if the target is a human from our coalition local target = weapon:getTarget() local targetUnit = Unit.getByName(target:getName()) veafSanctuary.recordTraceShooting(string.format("targetUnit=%s", veaf.p(veaf.ifnns(targetUnit, {"getID", "getName", "getTypeName", "getPlayerName", "getCoalition"})))) if targetUnit and targetUnit:getCoalition() == self:getCoalition() then local targetPlayername = targetUnit:getPlayerName() -- if the target is AI, then protect it anyway (protect assets, prevent bases bombing) if not targetPlayername or targetPlayername == "" then targetPlayername = "AI-target" end -- TODO debug - REMOVE LATER veafSanctuary.recordTraceShooting(string.format("targetPlayername=%s", veaf.p(targetPlayername))) if targetPlayername and targetPlayername ~= "" then -- check if the target is in the zone local position = targetUnit:getPosition().p veafSanctuary.recordTraceShooting(string.format("position=%s", veaf.p(position))) local inZone = self:isPositionInZone(position) if inZone then -- destroy the weapon with flak - :destroy() does not work for human players and weapons in MP veafSpawn.destroyObjectWithFlak(weapon, 1) -- warn the target local message = string.format(self:getMessageShotTarget(), targetPlayername, launcherPlayername) veafSanctuary.recordAction(string.format("Issuing a warning to target : %s", message)) trigger.action.outTextForGroup(targetUnit:getGroup():getID(), message, veafSanctuary.MESSAGE_TIME) -- count the offence local count = self.offensesByOffender[launcherPlayername] if not self.offensesByOffender[launcherPlayername] then self.offensesByOffender[launcherPlayername] = 1 else self.offensesByOffender[launcherPlayername] = self.offensesByOffender[launcherPlayername] + 1 end veafSanctuary.recordTraceShooting(string.format("self.offensesByOffender[launcherPlayername]=%s", veaf.p(self.offensesByOffender[launcherPlayername]))) if self.offensesByOffender[launcherPlayername] >= self:getOffensesBeforeDestruction() then -- destroy the offender local message = string.format("Instantly killing unit %s, too many offenses agains players in zone %s", launcherPlayername, self:getName()) trigger.action.outTextForCoalition(self:getCoalition(), message, veafSanctuary.MESSAGE_TIME) veafSanctuary.recordAction(message) -- flak the plane - :destroy() does not work for human players and weapons in MP veafSpawn.destroyObjectWithFlak(launcherUnit, 2, 2) -- forgive the player in 10 minutes (let him get out of trouble and don't kill him straight if he comes back) mist.scheduleFunction(VeafSanctuaryZone.forgive, {self, launcherPlayername}, timer.getTime() + veafSanctuary.FORGIVE_SHOOTER_AFTER) else -- warn the launcher local message = string.format(self:getMessageShotLauncher(), launcherPlayername, targetPlayername) veafSanctuary.recordAction(string.format("Issuing a warning to shooter : %s", message)) trigger.action.outTextForGroup(launcherUnit:getGroup():getID(), message, veafSanctuary.MESSAGE_TIME) end end end end end end end end function VeafSanctuaryZone:handleUnit(unit, data) veafSanctuary.recordTraceTrespassing(string.format("VeafSanctuaryZone[%s]:handleUnit()", veaf.p(self.name))) if not(unit) then return end local coalition = unit:getCoalition() if coalition == self:getCoalition() then veafSanctuary.recordTraceTrespassing(string.format("We're not concerned by this unit")) return -- we're not concerned by this unit end local position = unit:getPosition().p veafSanctuary.recordTraceTrespassing(string.format("position=%s", veaf.p(position))) local inZone = self:isPositionInZone(position) veafSanctuary.recordTraceTrespassing(string.format("inZone=%s", veaf.p(inZone))) -- let's decide what we do if inZone then local firstInZone = data.firstInZone if firstInZone < 0 then firstInZone = timer.getTime() data.firstInZone = firstInZone end local unitname = unit:getName() local playername = unit:getPlayerName() local callsign = unit:getCallsign() local timeInZone = timer.getTime() - firstInZone veafSanctuary.recordTraceTrespassing(string.format("unitname=%s, playername=%s, callsign=%s", veaf.p(unitname), veaf.p(playername), veaf.p(callsign))) local message = string.format("Unit %s is in the %s zone since %d seconds", playername, self:getName(), timeInZone) trigger.action.outTextForCoalition(self:getCoalition(), message, veafSanctuary.MESSAGE_TIME) veafSanctuary.recordAction(message) local groupId = unit:getGroup():getID() if self:getDelayInstant() > -1 and timeInZone >= self:getDelayInstant() then -- insta-death ! local message = string.format("Instantly killing unit %s, in zone %s since %d seconds", playername, self:getName(), timeInZone) trigger.action.outTextForCoalition(self:getCoalition(), message, veafSanctuary.MESSAGE_TIME) veafSanctuary.recordAction(message) -- flak the plane - :destroy() does not work for human players and weapons in MP veafSpawn.destroyObjectWithFlak(unit, 2, 2) elseif self:getDelaySpawn() > -1 and timeInZone >= self:getDelaySpawn() then -- spawn defense systems self:deployDefenses(position, unit, timeInZone) local message = string.format("Spawning defense systems to fend off unit %s, in zone %s since %d seconds", playername, self:getName(), timeInZone) veafSanctuary.recordAction(string.format("Issuing a warning to protected coalition : %s", message)) trigger.action.outTextForCoalition(self:getCoalition(), message, veafSanctuary.MESSAGE_TIME) local message = string.format("CRITICAL: %s - %s", playername, self:getMessageSpawn()) veafSanctuary.recordAction(string.format("Issuing a warning to trespasser : %s", message)) trigger.action.outTextForGroup(groupId, message, veafSanctuary.MESSAGE_TIME) elseif self:getDelayWarning() > -1 and timeInZone >= self:getDelayWarning() then -- simple warning local delay = self:getDelayInstant() if delay < 0 or (self:getDelaySpawn() > 0 and self:getDelaySpawn() < delay) then delay = self:getDelaySpawn() end local message = string.format(self:getMessageWarning(), playername, delay - timeInZone) veafSanctuary.recordAction(string.format("Issuing a warning to trespasser : %s", message)) trigger.action.outTextForGroup(groupId, message, veafSanctuary.MESSAGE_TIME) end elseif data.firstInZone >= 0 then local playername = unit:getPlayerName() -- reset the counter local message = string.format("%s got out of the zone", veaf.p(playername)) veafSanctuary.recordAction(message) data.firstInZone = -1 end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- core functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- add a zone function veafSanctuary.addZone(zone) veaf.loggers.get(veafSanctuary.Id):trace(string.format("addZone(%s)", veaf.p(zone:getName()))) table.insert(veafSanctuary.zonesList, zone) return zone end -- add a zone from a DCS trigger zone function veafSanctuary.addZoneFromTriggerZone(triggerZoneName) veaf.loggers.get(veafSanctuary.Id):trace(string.format("addZoneFromTriggerZone(%s)", veaf.p(triggerZoneName))) local triggerZone = trigger.misc.getZone(triggerZoneName) if triggerZoneName then local zone = VeafSanctuaryZone:new():setName(triggerZoneName):setRadius(triggerZone.radius):setPosition(triggerZone.point) return veafSanctuary.addZone(zone) end end -- Handle world events. veafSanctuary.eventHandler = {} function veafSanctuary.eventHandler:onEvent(event) if event == nil or event.id == nil then return end if not( event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_BIRTH or event.id == world.event.S_EVENT_DEAD or event.id == world.event.S_EVENT_SHOT ) then return end if (event.id == world.event.S_EVENT_SHOT) then -- process shooting events veafSanctuary.recordTraceShooting("S_EVENT_SHOT !") -- process all zones for _, zone in pairs(veafSanctuary.zonesList) do veaf.loggers.get(veafSanctuary.Id):trace(string.format("zone:getName()=%s", veaf.p(zone:getName()))) mist.scheduleFunction(VeafSanctuaryZone.handleWeapon, {zone, event.weapon}, timer.getTime() + veafSanctuary.DESTROY_WEAPONS_AFTER) end else -- process human players events if not event.initiator then return end local eventId = event.id if veaf.EVENTMETA[event.id] then eventId = veaf.EVENTMETA[event.id].Text end local _unitname = nil if event and event.initiator and event.initiator.getName then _unitname = event.initiator:getName() end --veaf.loggers.get(veafSanctuary.Id):trace(string.format("event initiator unit = %s", veaf.p(_unitname))) if event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_BIRTH and _unitname and veafSanctuary.humanUnits[_unitname] then veafSanctuary.recordTrace(string.format("event=%s",veaf.p(eventId))) if (not veafSanctuary.humanUnitsToFollow[_unitname]) then -- register the human unit in the follow-up list when the human gets in the unit veafSanctuary.recordTrace(string.format("registering human unit to follow: %s", veaf.p(_unitname))) veafSanctuary.humanUnitsToFollow[_unitname or ""] = { firstInZone = -1} end elseif event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD and _unitname and veafSanctuary.humanUnits[_unitname] then veafSanctuary.recordTrace(string.format("event=%s",veaf.p(eventId))) if (veafSanctuary.humanUnitsToFollow[_unitname]) then -- unregister the human unit from the follow-up list when the human gets in the unit veafSanctuary.recordTrace(string.format("deregistering human unit to follow: %s", veaf.p(_unitname))) veafSanctuary.humanUnitsToFollow[_unitname or ""] = nil end end end end -- main loop function veafSanctuary.loop() veaf.loggers.get(veafSanctuary.Id):debug("veafSanctuary.loop()") -- process all zones for _, zone in pairs(veafSanctuary.zonesList) do veaf.loggers.get(veafSanctuary.Id):trace(string.format("zone:getName()=%s", veaf.p(zone:getName()))) zone:cleanupDefenses() -- browse all the human units and check if they're in a zone for name, data in pairs(veafSanctuary.humanUnitsToFollow) do veaf.loggers.get(veafSanctuary.Id):trace(string.format("name=%s", veaf.p(name))) local unit = Unit.getByName(name) if unit then zone:handleUnit(unit, data) else -- stop following this unit, it's been destroyed veafSanctuary.humanUnitsToFollow[name] = nil end end end mist.scheduleFunction(veafSanctuary.loop, {}, timer.getTime() + veafSanctuary.DelayBetweenChecks) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafSanctuary.initialize() veafSanctuary.recordAction("Initializing module") -- prepare humans units veafSanctuary.humanUnits = {} for name, _ in pairs(mist.DBs.humansByName) do --veaf.loggers.get(veafSanctuary.Id):trace(string.format("mist.DBs.humansByName[%s]=??", veaf.p(name))) veafSanctuary.humanUnits[name] = true end --- Add the event handler. world.addEventHandler(veafSanctuary.eventHandler) -- Start the main loop mist.scheduleFunction(veafSanctuary.loop, {}, timer.getTime() + veafSanctuary.DelayForStartup) veafSanctuary.initialized = true veaf.loggers.get(veafSanctuary.Id):info(string.format("Sanctuary system has been initialized")) end veaf.loggers.get(veafSanctuary.Id):info(string.format("Loading version %s", veafSanctuary.Version)) ------------------ END script veafSanctuary.lua ------------------ ------------------ START script veafSkynetIadsHelper.lua ------------------ ------------------------------------------------------------------ -- VEAF helper for Skynet-IADS -- By zip (2021) -- -- Features: -- --------- -- * This module offers support for integrating Skynet-IADS in a mission -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafSkynet = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafSkynet.Id = "SKYNET" --- Version. veafSkynet.Version = "3.1.1" -- trace level, specific to this module --veafSkynet.LogLevel = "trace" veaf.loggers.new(veafSkynet.Id, veafSkynet.LogLevel) -- delay before the mission groups are added to the IADS' at start veafSkynet.DelayForStartup = 1 -- delay before restarting the IADS when adding a single group veafSkynet.DelayForRestart = 20 -- maximum x or y (z in DCS) between a SAM site and it's point defenses in meters veafSkynet.MaxPointDefenseDistanceFromSite = 10000 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafSkynet.initialized = false --flag to know if all units present on the map should be loaded at init or not into their team's main IADS network veafSkynet.loadAllAtInit = { [tostring(coalition.side.BLUE)] = true, [tostring(coalition.side.RED)] = true } --table containing the default IADS network names initialized for each coalition veafSkynet.defaultIADS = { [tostring(coalition.side.BLUE)] = "blue iads", [tostring(coalition.side.RED)] = "red iads", } veafSkynet.iadsSamUnitsTypes = {} veafSkynet.iadsEwrUnitsTypes = {} veafSkynet.GroupIntegrationModes = { Strict = 0, -- groups will only be included in the skynet networks if they contains only units known to skynet Lenient = 1 -- groups will be included in the skynet networks even if some units are not known to skynet } veafSkynet.GroupIntegrationMode = veafSkynet.GroupIntegrationModes.Lenient -- lenient by default -- Management of point defences (Flogas) -- when active, veafSkynet will attempt to set eligible groups as point defence of nearby defencible groups -- as per Skynet database types : -- - "single" groups will be eligible to defend "complex" or "ewr" groups -- - "complex" groups will be eligible to defend "ewr" groups -- - "single" groups will never be defended veafSkynet.PointDefenceModes = { None = 0, -- point defences will not be created Skynet = 1, -- point defences will be defined with the skynet logic Dcs = 2 -- point defences will be defined as not a part of the skynet network and left to the dcs ai } veafSkynet.PointDefenceMode = veafSkynet.PointDefenceModes.None -- no point defences by default veafSkynet.DynamicSpawn = false -- false by default veafSkynet.SkynetElementStates = { Autonomous = 0, Live = 1, Dark = 2 } --table containing the structure of each IADS network, first level is accessed with the IADS name. This contains the .coalitionID of the network, the IADS network (.iads), the groups added to the network (.groups) stored by groupName, --wether this network should appear on the radio menu (.includeInRadio) and lastly if this network is in debug mode (.debugFlag). The groups store whether the group was .forceEwr or .pointDefense. veafSkynet.structure = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafSkynet.getStringSkynetElement(skynetElement) local s = skynetElement.dcsName if (not skynetElement.dcsRepresentation:isExist()) then return s .. " (dcs object does not exist)" end local id = skynetElement.dcsRepresentation:getID() local category = getmetatable(skynetElement.dcsRepresentation) local sCategory = "unknown" if (category == Group) then sCategory = "group" elseif (category == Unit) then sCategory = "unit" elseif (category == StaticObject) then sCategory = "static" end if (skynetElement.getNatoName) then s = s .. " [" .. skynetElement:getNatoName() .. "]" else s = s .. " [" .. skynetElement.typeName .. "]" end s = s .. " [id=" .. id .. "]" .. " [" .. sCategory .. "]" return s end function veafSkynet.getDcsGroupFromSkynetElement(skynetElement) if (skynetElement.dcsRepresentation and skynetElement.dcsRepresentation:isExist()) then local category = getmetatable(skynetElement.dcsRepresentation) if (category == Group) then return skynetElement.dcsRepresentation elseif (category == Unit) then return Unit.getGroup(skynetElement.dcsRepresentation) end end return nil end function veafSkynet.getSkynetData(skynetElement) local function skynetDatabaseMatchType(skynetElementTypeList, skynetDatabaseTypeList) if (skynetDatabaseTypeList and skynetElementTypeList and #skynetElementTypeList > 0) then for i = 1, #skynetElementTypeList do if (skynetDatabaseTypeList[skynetElementTypeList[i].typeName]) then return true end end end return false end for skynetDataName, skynetData in pairs(SkynetIADS.database) do -- first check the launchers as they are the most unique thing if(skynetDatabaseMatchType(skynetElement.launchers, skynetData["launchers"])) then veaf.loggers.get(veafSkynet.Id):trace("Matched by launcher : " .. veafSkynet.getStringSkynetElement(skynetElement) .. " > " .. skynetDataName) return skynetData end -- tracking and search radars can be used by multiple sites if(skynetDatabaseMatchType(skynetElement.trackingRadars, skynetData["trackingRadar"])) then veaf.loggers.get(veafSkynet.Id):trace("Matched by TR : " .. veafSkynet.getStringSkynetElement(skynetElement) .. " > " .. skynetDataName) return skynetData end if(skynetDatabaseMatchType(skynetElement.searchRadars, skynetData["searchRadar"])) then veaf.loggers.get(veafSkynet.Id):trace("Matched by SR : " .. veafSkynet.getStringSkynetElement(skynetElement) .. " > " .. skynetDataName) return skynetData end end veaf.loggers.get(veafSkynet.Id):trace("No match : " .. veafSkynet.getStringSkynetElement(skynetElement)) return nil end function veafSkynet.removeSkynetElement(skynetElement, veafSkynetNetwork) local iads = veafSkynetNetwork.iads veaf.loggers.get(veafSkynet.Id):trace("Remove skynet element [" .. veafSkynet.getStringSkynetElement(skynetElement) .. "]") local function _removeSkynetElementFromList(list, skynetElement) local iIndex = -1 if (list and #list > 0) then for i = 1, #list do if (list[i] == skynetElement) then iIndex = i break end end if (iIndex > 0) then table.remove(list, iIndex) end end end skynetElement:cleanUp() skynetElement:getDCSRepresentation():enableEmission(true) local list = iads.samSites veaf.loggers.get(veafSkynet.Id):trace("Sam sites count: " .. #list) _removeSkynetElementFromList(list, skynetElement) --_removeSkynetElementFromList(iads:getEarlyWarningRadars(), skynetElement) veaf.loggers.get(veafSkynet.Id):trace("Sam sites count: " .. #list) -- not removed here local dcsGroup = veafSkynet.getDcsGroupFromSkynetElement(skynetElement) veafSkynetNetwork.groups[dcsGroup:getName()] = nil end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- core functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- get the IADS network for a given name ("blue iads", "red iads"") function veafSkynet.getNetwork(networkName) local network = nil if networkName then network = veafSkynet.structure[networkName] end return network end -- get the IADS object for a given name ("blue iads", "red iads"") function veafSkynet.getIADS(networkName) local iads = nil local network = veafSkynet.getNetwork(networkName) if network then iads = network.iads end return iads end -- calling SkynetIADS:activate() after a delay, to avoid calling it at each time a group is added to the IADS function veafSkynet.delayedActivate(networkName) veaf.loggers.get(veafSkynet.Id):debug("veafSkynet.delayedActivate(%s)", veaf.p(networkName)) local network = veafSkynet.structure[networkName] if network then if network.delayedActivation then veaf.loggers.get(veafSkynet.Id):trace(string.format("IADS %s already has a delayed activation", veaf.p(networkName))) else veaf.loggers.get(veafSkynet.Id):trace(string.format("IADS %s will be activated in %d seconds", veaf.p(networkName), veafSkynet.DelayForRestart)) network.delayedActivation = mist.scheduleFunction(veafSkynet._activateIADS, {networkName}, timer.getTime() + veafSkynet.DelayForRestart) end end end function veafSkynet._activateIADS(networkName) veaf.loggers.get(veafSkynet.Id):debug("veafSkynet._activateIADS(%s)", veaf.p(networkName)) local network = veafSkynet.structure[networkName] if network then network.delayedActivation = nil local iads = network.iads if iads then veaf.loggers.get(veafSkynet.Id):debug("calling iads:activate()") iads:activate() end end end function veafSkynet.getIadsOfCoalition(networkName, coa) local iads = nil if veafSkynet.structure[networkName] and coa == veafSkynet.structure[networkName].coalitionID then iads = veafSkynet.structure[networkName].iads end return iads end function veafSkynet.getNearestIADSSite(networkName, dcsGroup) if not dcsGroup then veaf.loggers.get(veafSkynet.Id):trace("No group to find the nearest IADS site for") return false end local coa = dcsGroup:getCoalition() veaf.loggers.get(veafSkynet.Id):trace(string.format("Ref coalition : %s", veaf.p(coa))) local iads = veafSkynet.getIadsOfCoalition(networkName, coa) if not iads then veaf.loggers.get(veafSkynet.Id):trace(string.format("IADS named %s for the coalition of the group %s does not exist", veaf.p(networkName), tostring(dcsGroup:getName()))) return false end local currentGroup = dcsGroup:getName() veaf.loggers.get(veafSkynet.Id):trace(string.format("networkName : %s", veaf.p(networkName))) local groupPos = veaf.getAveragePosition(dcsGroup) veaf.loggers.get(veafSkynet.Id):debug(string.format("Ref Position : %s", veaf.p(groupPos))) local nearestEWRname = nil local minEWRDistance = veafSkynet.MaxPointDefenseDistanceFromSite local nearestSAMname = nil local minSAMDistance = veafSkynet.MaxPointDefenseDistanceFromSite local CoalitionSites = nil local searchForGroup = function(CoalitionSites, pos, currentGroupName, ewrFlag) local minDistance = veafSkynet.MaxPointDefenseDistanceFromSite local FoundGroup = nil for site, site_info in pairs(CoalitionSites) do local site_name = site_info.dcsName -- For EWRs it looks like this gives the unit's name which would need to be reversed to the group to get position data veaf.loggers.get(veafSkynet.Id):trace(string.format("Checked Site groupName : %s and isEWR : %s", veaf.p(site_name), veaf.p(ewrFlag))) if site_name and currentGroupName ~= site_name then if ewrFlag then local unit = Unit.getByName(site_name) local group = Unit.getGroup(unit) site_name = Group.getName(group) end veaf.loggers.get(veafSkynet.Id):trace(string.format("Checked Site groupName : %s", veaf.p(site_name))) local groupAvgPosition = veaf.getAveragePosition(site_name) veaf.loggers.get(veafSkynet.Id):debug(string.format("Checked Site groupAvgPosition : %s", veaf.p(groupAvgPosition))) if groupAvgPosition then local distance = math.sqrt((pos.x-groupAvgPosition.x)^2+(pos.z-groupAvgPosition.z)^2) veaf.loggers.get(veafSkynet.Id):trace(string.format("Distance between checked site and pointDefense : %s", veaf.p(distance))) if distance <= minDistance then veaf.loggers.get(veafSkynet.Id):trace("This site is closer") FoundGroup = site_name minDistance = distance end end end end return FoundGroup, minDistance end --start by going through the EWRs CoalitionSites = iads:getEarlyWarningRadars() nearestEWRname, minEWRDistance = searchForGroup(CoalitionSites, groupPos, currentGroup, true) --search for SAM sites CoalitionSites = iads:getSAMSites() nearestSAMname, minSAMDistance = searchForGroup(CoalitionSites, groupPos, currentGroup) if minEWRDistance <= minSAMDistance then return nearestEWRname end return nearestSAMname end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Management of point defences (Flogas) ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafSkynet.canBePointDefence(skynetData) if (skynetData == nil) then return false end if (skynetData["can_engage_harm"] ~= true) then return false end if (skynetData["type"] ~= "complex" and skynetData["type"] ~= "single") then return false end return true end function veafSkynet.findSkynetElementToDefend(skynetElementPointDefence, skynetDataPointDefence) local dcsGroupPointDefence = veafSkynet.getDcsGroupFromSkynetElement(skynetElementPointDefence) local iads = skynetElementPointDefence.iads local pointDefenceType = skynetDataPointDefence["type"] local pointDefencePosition = veaf.getAveragePosition(dcsGroupPointDefence) if (pointDefencePosition == nil) then return nil end local function findClosestSkynetElementInList(skynetElementList) if (skynetElementList == nil or #skynetElementList <= 0) then return nil end local iMinDistance = veafSkynet.MaxPointDefenseDistanceFromSite local foundElement for i = 1, #skynetElementList do local skynetElement = skynetElementList[i] local skynetData = veafSkynet.getSkynetData(skynetElement) if (skynetData and (skynetData["type"] == "ewr" or (skynetData["type"] == "complex" and pointDefenceType == "single"))) then -- ewr are always defencible, and complex sam sites only defencible by single groups local dcsGroup = veafSkynet.getDcsGroupFromSkynetElement(skynetElementList[i]) local position = veaf.getAveragePosition(dcsGroup) if (position) then local iDistance = math.sqrt((position.x - pointDefencePosition.x)^2 + (position.z - pointDefencePosition.z)^2) if (iDistance < iMinDistance) then foundElement = skynetElement iMinDistance = iDistance end end end end return foundElement end local elementToDefend = findClosestSkynetElementInList (iads:getEarlyWarningRadars()) if (elementToDefend == nil and pointDefenceType == "single") then elementToDefend = findClosestSkynetElementInList (iads:getSAMSites()) end if (elementToDefend) then local sDescriptorPointDefence = veafSkynet.getStringSkynetElement(skynetElementPointDefence) local sDescriptorToDefend = veafSkynet.getStringSkynetElement(elementToDefend) veaf.loggers.get(veafSkynet.Id):trace("Identified [ " .. sDescriptorToDefend .. " ] to be defended by [ " .. sDescriptorPointDefence .. " ]") end return elementToDefend end function veafSkynet.removePointDefencesFromSkynetElement(skynetElement) if(skynetElement and skynetElement.pointDefences and #skynetElement.pointDefences > 0) then for i = 1, #skynetElement.pointDefences do skynetElement.pointDefences[i]:setIsAPointDefence(false) end skynetElement.pointDefences = {} end skynetElement.pointDefences = {} end function veafSkynet.removePointDefences(iads) -- not tested local ewrs = iads:getEarlyWarningRadars() if (ewrs and #ewrs > 0) then for i = 1, #ewrs do veafSkynet.removePointDefencesFromSkynetElement(ewrs[i]) end end local samSites = iads:getSAMSites() if (samSites and #samSites > 0) then for i = 1, #samSites do veafSkynet.removePointDefencesFromSkynetElement(samSites[i]) end end end function veafSkynet.initializePointDefenceSamSite(samSite, veafSkynetNetwork) if (samSite:getIsAPointDefence()) then return end local skynetData = veafSkynet.getSkynetData(samSite) if (not veafSkynet.canBePointDefence(skynetData)) then return end local elementToDefend = veafSkynet.findSkynetElementToDefend(samSite, skynetData) if (elementToDefend) then if (veafSkynet.PointDefenceMode == veafSkynet.PointDefenceModes.Skynet) then veaf.loggers.get(veafSkynet.Id):debug("Point defence: add as skynet") elementToDefend:addPointDefence(samSite) elseif (veafSkynet.PointDefenceMode == veafSkynet.PointDefenceModes.Dcs) then veaf.loggers.get(veafSkynet.Id):debug("Point defence: add as dcs") veafSkynet.removeSkynetElement(samSite, veafSkynetNetwork) end end end function veafSkynet.initializePointDefences(veafSkynetNetwork) if (veafSkynet.PointDefenceMode ~= veafSkynet.PointDefenceModes.Skynet and veafSkynet.PointDefenceMode ~= veafSkynet.PointDefenceModes.Dcs) then return end veaf.loggers.get(veafSkynet.Id):debug("Analyzing network to create point defences") local iads = veafSkynetNetwork.iads local samSites = iads:getSAMSites() if (samSites and #samSites > 0) then for i = 1, #samSites do veafSkynet.initializePointDefenceSamSite(samSites[i], veafSkynetNetwork) end end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Management of dynamic group spawns (Flogas) -- Option to integrate late spawned units into the existing networks ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafSkynet.monitorDynamicSpawnHandlerId = nil function veafSkynet.OnDynamicSpawn(event) if(not veafSkynet.initialized) then return end if(event.id ~= world.event.S_EVENT_BIRTH) then return end if (event.initiator == nil or Object.getCategory(event.initiator) ~= Object.Category.UNIT) then return end -- birth event will be triggered for each unit of a spawned group, but we want to manage the group, so we only work for for the first unit local dcsGroup = Unit.getGroup (event.initiator) local firstDcsUnit = dcsGroup:getUnit(1) if(firstDcsUnit == nil) then return end if(firstDcsUnit:getID() ~= event.initiator:getID()) then return end local coalition = dcsGroup:getCoalition() local networkName = veafSkynet.defaultIADS[tostring(coalition)] if not networkName then veaf.loggers.get(veafSkynet.Id):error("No default IADS network for coalition " .. coalition) return end veaf.loggers.get(veafSkynet.Id):debug("DYNAMIC SPAWN adding spawned group [" .. dcsGroup:getName() .. "] [id=" .. dcsGroup:getID() .. "] to IADS network [" .. networkName .. "]") if (veafSkynet.addGroupToNetwork(networkName, dcsGroup, false, false)) then veafSkynet.initializePointDefences(veafSkynet.getNetwork(networkName)) --iads:buildRadarCoverage() end end function veafSkynet.monitorDynamicSpawn(bMonitor) if (bMonitor) then if (veafSkynet.monitorDynamicSpawnHandlerId ~= nil) then return -- already active end veaf.loggers.get(veafSkynet.Id):debug("DYNAMIC SPAWN monitoring ON") veafSkynet.monitorDynamicSpawnHandlerId = mist.addEventHandler(veafSkynet.OnDynamicSpawn) else if (veafSkynet.monitorDynamicSpawnHandlerId == nil) then return -- already inactive end veaf.loggers.get(veafSkynet.Id):debug("DYNAMIC SPAWN monitoring OFF") mist.removeEventHandler(veafSkynet.monitorDynamicSpawnHandlerId) veafSkynet.monitorDynamicSpawnHandlerId = nil end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafSkynet.isGroupUsable(dcsGroup) if (veafSkynet.GroupIntegrationMode == veafSkynet.GroupIntegrationModes.Strict) then for _, dcsUnit in pairs(dcsGroup:getUnits()) do local dcsUnitType = dcsUnit:getTypeName() if (not veafSkynet.iadsEwrUnitsTypes[dcsUnitType] and not veafSkynet.iadsSamUnitsTypes[dcsUnitType]) then return false end end return true else for _, dcsUnit in pairs(dcsGroup:getUnits()) do local dcsUnitType = dcsUnit:getTypeName() if (veafSkynet.iadsEwrUnitsTypes[dcsUnitType] or veafSkynet.iadsSamUnitsTypes[dcsUnitType]) then return true end end return false end end function veafSkynet.addGroupToNetwork(networkName, dcsGroup, forceEwr, pointDefense, alreadyAddedGroups, silent) veaf.loggers.get(veafSkynet.Id):debug("ADD GROUP START [" .. dcsGroup:getName() .. "] [id=" .. dcsGroup:getID() .. "] to IADS network [" .. networkName .. "]") if not dcsGroup then veaf.loggers.get(veafSkynet.Id):error("No group to find to add to network") return false end if (not veafSkynet.isGroupUsable(dcsGroup)) then veaf.loggers.get(veafSkynet.Id):trace("Group is not usable for skynet") return false end local forceEwr = false or forceEwr local pointDefense = false or pointDefense local silent = false or silent local batchMode = (alreadyAddedGroups ~= nil) local alreadyAddedGroups = alreadyAddedGroups or {} local groupName = dcsGroup:getName() local coa = dcsGroup:getCoalition() local iads = veafSkynet.getIadsOfCoalition(networkName, coa) if not(iads) then veaf.loggers.get(veafSkynet.Id):debug(string.format("IADS named %s for the coalition of the group %s does not exist", veaf.p(networkName), tostring(groupName))) return false end local didSomething = false veaf.loggers.get(veafSkynet.Id):trace(string.format("batchMode=%s forceEwr=%s PointDefense=%s", tostring(batchMode), tostring(forceEwr), tostring(pointDefense))) local defended_name = nil if pointDefense and not(forceEwr) then veaf.loggers.get(veafSkynet.Id):trace(string.format("SAM is requested as pointDefense")) if pointDefense == true then veaf.loggers.get(veafSkynet.Id):trace(string.format("Find nearest site to defend")) defended_name = veafSkynet.getNearestIADSSite(networkName, dcsGroup) else defended_name = pointDefense local defended_SAM = iads:getSAMSiteByGroupName(defended_name) local defended_EWR = iads:getEarlyWarningRadars(defended_name) local defended_site = defended_EWR if defended_SAM then defended_site = defended_SAM end if defended_site then local defended_pos = veaf.getAvgGroupPos(defended_name) local dcsGroup_pos = veaf.getAvgGroupPos(groupName) local distance = math.sqrt((dcsGroup_pos.x-defended_pos.x)^2+(dcsGroup_pos.z-defended_pos.z)^2) veaf.loggers.get(veafSkynet.Id):trace(string.format("Distance between requested site and pointDefense : %s", veaf.p(distance))) if distance > veafSkynet.MaxPointDefenseDistanceFromSite then defended_name = nil veaf.loggers.get(veafSkynet.Id):info("User requested SAM Site out of reach for point defense") end else defended_name = nil end end end local dcsGroupName = dcsGroup:getName() for _, dcsUnit in pairs(dcsGroup:getUnits()) do local dcsUnitName = dcsUnit:getName() local dcsUnitType = dcsUnit:getTypeName() local addedSite = nil local sLog = string.format(" - iads[%s] unit[%s][%d][%s] group[%s][%d]", iads.name, dcsUnitName, dcsUnit:getID(), dcsUnitType, dcsGroupName, dcsGroup:getID()) if(not veafSkynet.iadsEwrUnitsTypes[dcsUnitType] and not veafSkynet.iadsSamUnitsTypes[dcsUnitType]) then veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => unit type is not eligible for skynet") elseif(alreadyAddedGroups[groupName]) then veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => group as already been marked as added by veafSkynet") break -- if group is added no need to go check the other units else local bShouldBeAdded = true local sams = iads:getSAMSites() for i = 1, #sams do local sam = sams[i] if (sam.dcsName == dcsGroupName) then veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => already in network as SAM group") bShouldBeAdded = false break end end local ewrs = iads:getEarlyWarningRadars() for i = 1, #ewrs do local ewr = ewrs[i] local dcsGroupEwr = veafSkynet.getDcsGroupFromSkynetElement(ewr) if (not dcsGroupEwr) then veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => group not found for skynet ewr " .. veafSkynet.getStringSkynetElement(ewr)) elseif (dcsGroupEwr:getName() == dcsGroupName) then veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => already in network as EWR group") bShouldBeAdded = false break end end if (not bShouldBeAdded) then break -- if group is added no need to go check the other units end if(veafSkynet.iadsSamUnitsTypes[dcsUnitType]) then local samsite = iads:addSAMSite(groupName) if (samsite) then addedSite = samsite didSomething = true alreadyAddedGroups[groupName] = true veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => added as SAM") end end local ewrFlag = false if (addedSite == nil and veafSkynet.iadsEwrUnitsTypes[dcsUnitType]) then local ewr = iads:addEarlyWarningRadar(dcsUnitName) if (ewr) then addedSite = ewr didSomething = true ewrFlag = true veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => added as EWR") end end --user requested configuration if addedSite and pointDefense and not(forceEwr) and not(ewrFlag) then if defended_name then local text = string.format("Point Defense added to site : %s", string.format(defended_name)) veaf.loggers.get(veafSkynet.Id):info(text) if not silent then trigger.action.outText(text,10) end if iads:getSAMSiteByGroupName(defended_name) then veaf.loggers.get(veafSkynet.Id):trace(string.format("adding pointDefense to SAM -> OK")) iads:getSAMSiteByGroupName(defended_name):addPointDefence(addedSite) else veaf.loggers.get(veafSkynet.Id):trace(string.format("adding pointDefense to EWR -> OK")) iads:getEarlyWarningRadars(defended_name):addPointDefence(addedSite) --confirm that the addition of point defenses to the EWR which is gathered through it's group name but only the unit name is stored in the structure --local site_info = iads:getEarlyWarningRadars(defended_name) --veaf.loggers.get(veafSkynet.Id):debug(string.format("Recovered EWR name : %s", veaf.p(site_info[1].dcsName))) --local pointDefenses = site_info[1].pointDefences --veaf.loggers.get(veafSkynet.Id):debug(string.format("Recover pointDefense name : %s", veaf.p(pointDefenses[#pointDefenses].dcsName))) end else veaf.loggers.get(veafSkynet.Id):info("Could not find SAM site within range to add point defenses to") if not silent then trigger.action.outText("Could not find SAM site within range to add point defenses to", 15) end end elseif forceEwr then veaf.loggers.get(veafSkynet.Id):trace(string.format("SAM/EWR is forced EWR")) if addedSite then veaf.loggers.get(veafSkynet.Id):trace("Unit Forced as EWR") addedSite:setActAsEW(true) end end if (addedSite) then break -- if something has been added for this group no need to check the remaining units end end end if didSomething then if not(batchMode) and not(forceEwr) and not(pointDefense) then -- specific configurations, for each SAM type veaf.loggers.get(veafSkynet.Id):trace("Specific configuration applied") iads:getSAMSitesByNatoName('SA-10'):setActAsEW(false) iads:getSAMSitesByNatoName('SA-6'):setActAsEW(false) iads:getSAMSitesByNatoName('SA-5'):setActAsEW(false) iads:getSAMSitesByNatoName('Patriot'):setActAsEW(false) iads:getSAMSitesByNatoName('Hawk'):setActAsEW(false) end -- reactivate (rebuild coverage) the IADS veaf.loggers.get(veafSkynet.Id):trace("reactivate (rebuild coverage) the IADS") veafSkynet.delayedActivate(networkName) --add the added site to the structure of the network it was added to veafSkynet.structure[networkName].groups[groupName] = { forceEwr = forceEwr, pointDefense = defended_name } end return didSomething end local function initializeIADS(networkName, coa, inRadio, debug) local iads = veafSkynet.getIadsOfCoalition(networkName, coa) if not(iads) then veaf.loggers.get(veafSkynet.Id):trace(string.format("IADS named %s for coalition %s does not exist", veaf.p(networkName), veaf.p(coa))) return false end veaf.loggers.get(veafSkynet.Id):trace(string.format("initializeIADS %s",tostring(iads:getCoalitionString()))) if debug then veaf.loggers.get(veafSkynet.Id):debug("adding debug information") local iadsDebug = iads:getDebugSettings() iadsDebug.IADSStatus = true iadsDebug.radarWentDark = true -- FG iadsDebug.samWentDark = true iadsDebug.contacts = true iadsDebug.radarWentLive = true iadsDebug.noWorkingCommmandCenter = false iadsDebug.ewRadarNoConnection = false iadsDebug.samNoConnection = false iadsDebug.jammerProbability = true iadsDebug.addedEWRadar = true iadsDebug.hasNoPower = false iadsDebug.harmDefence = true iadsDebug.samSiteStatusEnvOutput = true iadsDebug.earlyWarningRadarStatusEnvOutput = true end local alreadyAddedGroups = {} local dcsGroups = coalition.getGroups(coa) for _, dcsGroup in pairs(dcsGroups) do if veafSkynet.structure[networkName] then local groupName = dcsGroup:getName() if groupName then local structureData = veafSkynet.structure[networkName].groups[groupName] local forceEwr = false local pointDefense = false if structureData then if structureData.forceEwr then forceEwr = structureData.forceEwr end if structureData.pointDefense then pointDefense = structureData.pointDefense end end if veafSkynet.loadAllAtInit[tostring(coa)] or structureData then veafSkynet.addGroupToNetwork(networkName, dcsGroup, forceEwr, pointDefense, alreadyAddedGroups, true) end end end end if veafSkynet.loadAllAtInit[tostring(coa)] then veafSkynet.loadAllAtInit[tostring(coa)] = false end veaf.loggers.get(veafSkynet.Id):trace("Specific configuration applied") -- specific configurations, for each SAM type iads:getSAMSitesByNatoName('SA-10'):setActAsEW(false) iads:getSAMSitesByNatoName('SA-6'):setActAsEW(false) iads:getSAMSitesByNatoName('SA-5'):setActAsEW(false) iads:getSAMSitesByNatoName('Patriot'):setActAsEW(false) iads:getSAMSitesByNatoName('Hawk'):setActAsEW(false) veafSkynet.initializePointDefences(veafSkynet.getNetwork(networkName)) -- Management of point defences (Flogas) - initialization if inRadio then --activate the radio menu to toggle IADS Status output iads:addRadioMenu() end --activate (build coverage) the IADS veaf.loggers.get(veafSkynet.Id):debug("activate (build coverage) the IADS") veafSkynet.delayedActivate(networkName) end local function createNetwork(networkName, coa, loadUnits, UserAdd) local UserAdd = UserAdd or false local loadUnits = loadUnits or false local networkName = networkName if networkName then networkName = tostring(networkName) else veaf.loggers.get(veafSkynet.Id):error("networkName is of invalid format") return false end local coa = coa if coa then coa = tonumber(coa) else veaf.loggers.get(veafSkynet.Id):error("Coalition specified is of invalid format") return false end veaf.loggers.get(veafSkynet.Id):trace("networkName= %s", veaf.p(networkName)) veaf.loggers.get(veafSkynet.Id):trace("CoalitionID= %s", veaf.p(coa)) veaf.loggers.get(veafSkynet.Id):trace("loadUnits= %s", veaf.p(loadUnits)) veaf.loggers.get(veafSkynet.Id):trace("UserAdd= %s", veaf.p(UserAdd)) if networkName and coa then if (UserAdd and not veafSkynet.structure[networkName]) or not UserAdd then local debugFlag = veafSkynet.debugBlue local includeInRadio = veafSkynet.includeBlueInRadio if coa == coalition.side.RED then debugFlag = veafSkynet.debugRed includeInRadio = veafSkynet.includeRedInRadio end veaf.loggers.get(veafSkynet.Id):trace("creating network...") local iads = SkynetIADS:create(networkName) iads.coalitionID = coa if iads then if not veafSkynet.structure[networkName] then veaf.loggers.get(veafSkynet.Id):trace("network is new") veafSkynet.structure[networkName] = {} veafSkynet.structure[networkName].coalitionID = coa veafSkynet.structure[networkName].includeInRadio = includeInRadio veafSkynet.structure[networkName].debugFlag = debugFlag veafSkynet.structure[networkName].groups = {} end veafSkynet.structure[networkName].iads = iads if veaf.loggers.get(veafSkynet.Id):wouldLogTrace() then veaf.loggers.get(veafSkynet.Id):trace("Stored structure for network named %s :", veaf.p(networkName)) for index,_ in pairs(veafSkynet.structure[networkName]) do veaf.loggers.get(veafSkynet.Id):trace("-> %s", veaf.p(index)) end veaf.loggers.get(veafSkynet.Id):trace("Stored IADS structure for network named %s :", veaf.p(networkName)) for index,_ in pairs(veafSkynet.structure[networkName].iads) do veaf.loggers.get(veafSkynet.Id):trace("-> %s", veaf.p(index)) end veaf.loggers.get(veafSkynet.Id):trace("CoalitionID for network named %s :", veaf.p(networkName)) veaf.loggers.get(veafSkynet.Id):trace("-> %s", veaf.p(veafSkynet.structure[networkName].iads.coalitionID)) veaf.loggers.get(veafSkynet.Id):trace("-> %s", veaf.p(veafSkynet.structure[networkName].iads:getCoalitionString())) end if loadUnits then initializeIADS(networkName, coa, includeInRadio, debugFlag) end return true end else local text = string.format("The network name \"%s\" already exists", veaf.p(networkName)) veaf.loggers.get(veafSkynet.Id):info(text) end end return false end -- reset an IADS network, useful when many additions are made at once to harmonize the structure function veafSkynet.reinitializeNetwork(networkName) if not veafSkynet.initialized then return false end if networkName and veafSkynet.structure[networkName] then local networkStructure = veafSkynet.structure[networkName] if networkStructure.iads then veaf.loggers.get(veafSkynet.Id):trace("Stored structure for network named %s has IADS, deactivating", veaf.p(networkName)) if networkStructure.includeInRadio then veaf.loggers.get(veafSkynet.Id):trace("Removing radio menu...") networkStructure.iads:removeRadioMenu() end networkStructure.iads:deactivate() end createNetwork(networkName, networkStructure.coalitionID, true) end end -- reset an IADS networks, useful when many additions/destructions are made at once to harmonize the structures on the skynet side function veafSkynet.reinitialize() if not veafSkynet.initialized then return false end for networkName,_ in pairs(veafSkynet.structure) do veafSkynet.reinitializeNetwork(networkName) end end function veafSkynet.initialize(includeRedInRadio, debugRed, includeBlueInRadio, debugBlue) veaf.loggers.get(veafSkynet.Id):info(string.format("initializing Skynet in %s seconds", tostring(veafSkynet.DelayForStartup))) mist.scheduleFunction(veafSkynet._initialize,{includeRedInRadio, debugRed, includeBlueInRadio, debugBlue}, timer.getTime()+veafSkynet.DelayForStartup) end function veafSkynet._initialize(includeRedInRadio, debugRed, includeBlueInRadio, debugBlue) veafSkynet.includeRedInRadio = includeRedInRadio or false veafSkynet.debugRed = debugRed or false veafSkynet.includeBlueInRadio = includeBlueInRadio or false veafSkynet.debugBlue = debugBlue or false veaf.loggers.get(veafSkynet.Id):info("Initializing module") veaf.loggers.get(veafSkynet.Id):debug(string.format("includeRedInRadio=%s",veaf.p(includeRedInRadio))) veaf.loggers.get(veafSkynet.Id):debug(string.format("debugRed=%s",veaf.p(debugRed))) veaf.loggers.get(veafSkynet.Id):debug(string.format("includeBlueInRadio=%s",veaf.p(includeBlueInRadio))) veaf.loggers.get(veafSkynet.Id):debug(string.format("debugBlue=%s",veaf.p(debugBlue))) -- prepare the list of units supported by Skynet IADS for _, groupData in pairs(SkynetIADS.database) do for _, listName in pairs({ "searchRadar", "trackingRadar", "launchers", "misc" }) do if groupData['type'] ~= 'ewr' then local list = groupData[listName] if list then for unitType, _ in pairs(list) do veaf.loggers.get(veafSkynet.Id):trace(string.format("-> SAM")) veafSkynet.iadsSamUnitsTypes[unitType] = true end end end end end veaf.loggers.get(veafSkynet.Id):trace(string.format("veafSkynet.iadsSamUnitsTypes=%s",veaf.p(veafSkynet.iadsSamUnitsTypes))) -- add EWR-capable units for _, unit in pairs(dcsUnits.DcsUnitsDatabase) do if unit then veaf.loggers.get(veafSkynet.Id):trace(string.format("testing unit %s",veaf.p(unit.type))) if unit.attribute then veaf.loggers.get(veafSkynet.Id):trace(string.format("unit.attribute = %s",veaf.p(unit.attribute))) if (unit.attribute["SAM SR"]) then veafSkynet.iadsEwrUnitsTypes[unit.type] = true veaf.loggers.get(veafSkynet.Id):trace(string.format("-> EWR")) elseif (unit.attribute["EWR"]) then veafSkynet.iadsEwrUnitsTypes[unit.type] = true veaf.loggers.get(veafSkynet.Id):trace(string.format("-> EWR")) elseif (unit.attribute["AWACS"]) then veafSkynet.iadsEwrUnitsTypes[unit.type] = true veaf.loggers.get(veafSkynet.Id):trace(string.format("-> EWR")) end end end end veaf.loggers.get(veafSkynet.Id):trace(string.format("veafSkynet.iadsEwrUnitsTypes=%s",veaf.p(veafSkynet.iadsEwrUnitsTypes))) veaf.loggers.get(veafSkynet.Id):info("Creating IADS for BLUE") createNetwork(veafSkynet.defaultIADS[tostring(coalition.side.BLUE)], coalition.side.BLUE, true) veaf.loggers.get(veafSkynet.Id):info("Creating IADS for RED") createNetwork(veafSkynet.defaultIADS[tostring(coalition.side.RED)], coalition.side.RED, true) veafSkynet.initialized = true if (veafSkynet.CommandCentersPreinitialize and #veafSkynet.CommandCentersPreinitialize > 0) then for _, commandCenter in pairs(veafSkynet.CommandCentersPreinitialize) do veafSkynet.addCommandCenterOfCoalition(commandCenter.CoalitionId, commandCenter.CommandCenterName) end veafSkynet.CommandCentersPreinitialize = {} end if(veafSkynet.DynamicSpawn) then veafSkynet.monitorDynamicSpawn(true) end veaf.loggers.get(veafSkynet.Id):info(string.format("Skynet IADS has been initialized")) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Command centers and network deactivation ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafSkynet.CommandCentersPreinitialize = {} -- this is to memorize the command centers requested for a coalition, if the network of the coalition does not exist yet function veafSkynet.addCommandCenter(veafSkynetNetwork, sCommandCenterName) local iads = veafSkynetNetwork.iads local dcsCommandCenterObject = StaticObject.getByName(sCommandCenterName) if (not dcsCommandCenterObject) then dcsCommandCenterObject = Unit.getByName(sCommandCenterName) end if (not dcsCommandCenterObject) then local dcsCommandCenterGroup = Group.getByName(sCommandCenterName) if (dcsCommandCenterGroup) then dcsCommandCenterObject = dcsCommandCenterGroup:getUnit(1) end end if (not dcsCommandCenterObject) then veaf.loggers.get(veafSkynet.Id):error("Requested command center not found: " .. sCommandCenterName) return end iads:addCommandCenter(dcsCommandCenterObject) veaf.loggers.get(veafSkynet.Id):trace("Command center unit added [" .. sCommandCenterName .. "]") iads:buildRadarCoverage() -- as command center is added after the network initialisation, coverage should be rebuilt end function veafSkynet.addCommandCenterOfCoalition(iCoalitionId, sCommandCenterName) if (veafSkynet.initialized) then local veafSkynetNetwork = veafSkynet.getNetwork(veafSkynet.defaultIADS[tostring(iCoalitionId)]) if (veafSkynetNetwork == nil) then veaf.loggers.get(veafSkynet.Id):error("Veaf skynet network not found. Please ensure that veafSkynetIadsHelper has been initialized for coalition [" .. iCoalitionId .. "]") return end veafSkynet.addCommandCenter(veafSkynetNetwork, sCommandCenterName) else veaf.loggers.get(veafSkynet.Id):trace("Veaf skynet not initialized. Command center [" .. sCommandCenterName .. "] stored to be added later for [" .. iCoalitionId .. "]") table.insert (veafSkynet.CommandCentersPreinitialize, {CoalitionId = iCoalitionId, CommandCenterName = sCommandCenterName}) end end function veafSkynet.destroyCommandCenters(veafSkynetNetwork, iExplosionStrength) iExplosionStrength = iExplosionStrength or 200 -- default explosion may not be enough to destroy certain bunkers local iads = veafSkynetNetwork.iads if (not iads:isCommandCenterUsable()) then veaf.loggers.get(veafSkynet.Id):trace("Network has no usable command center") return end local ccs = iads:getCommandCenters() for i = 1, #ccs do local cc = ccs[i] local dcsObject = cc.dcsRepresentation if (dcsObject:isExist()) then local category = getmetatable(dcsObject) if (category == Group) then for _, dcsUnit in pairs(dcsObject:getUnits()) do veaf.loggers.get(veafSkynet.Id):trace("Command center unit exploded: " .. dcsUnit:getName()) trigger.action.explosion(dcsUnit:getPosition().p, iExplosionStrength) end else veaf.loggers.get(veafSkynet.Id):trace("Command center unit exploded: " .. dcsObject:getName()) trigger.action.explosion(dcsObject:getPosition().p, iExplosionStrength) end end end end function veafSkynet.destroyCommandCentersOfCoalition(iCoalitionId, iExplosionStrength) local veafSkynetNetwork = veafSkynet.getNetwork(veafSkynet.defaultIADS[tostring(iCoalitionId)]) veafSkynet.destroyCommandCenters(veafSkynetNetwork, iExplosionStrength) end function veafSkynet.deactivateNetwork(veafSkynetNetwork, elementStates) local elementState = elementStates or veafSkynet.SkynetElementStates.Live local iads = veafSkynetNetwork.iads local sElementState = "live" if (elementState == veafSkynet.SkynetElementStates.Autonomous) then sElementState = "autonomous" elseif (elementState == veafSkynet.SkynetElementStates.Dark) then sElementState = "dark" end veaf.loggers.get(veafSkynet.Id):trace("Deactivating network " .. iads:getCoalitionString() .. ". Network elements will go " .. sElementState) local function setGroupState(skynetElement) skynetElement:finishHarmDefence() skynetElement:cleanUp() veafSkynet.removePointDefencesFromSkynetElement(skynetElement) if (elementState == veafSkynet.SkynetElementStates.Autonomous) then skynetElement:resetAutonomousState() skynetElement:goAutonomous() elseif (elementState == veafSkynet.SkynetElementStates.Dark) then skynetElement:goDark() -- goDark will not always turn the radar off - eg an EWR that is tracking targets will stay on else skynetElement:goLive() -- goLive will not always turn the radar on - eg a SAM site out of ammo will stay off end end veafSkynet.monitorDynamicSpawn(false) iads:deactivate() local ewrs = iads:getEarlyWarningRadars() for i = 1, #ewrs do local ewr = ewrs[i] setGroupState(ewr) end local sams = iads:getSAMSites() for i = 1, #sams do local sam = sams[i] setGroupState(sam) end -- Copying the elements and emptying the Skynet lists before doing the state switch to ensure that Skynet does not keep controlling the elements after deactivation. -- Does not seem necessary after all, and keeping the lists in the Skynet network allows to reactivate it later if needed. -- Note that as it is, reactivating will not rebuild point defences, so it is more of a testing thing still. -- Code kept here just in case. --[[ local skynetGroups = {} for i = 1, #iads.earlyWarningRadars do table.insert(skynetGroups, iads.earlyWarningRadars[i]) end for i = 1, #iads.samSites do table.insert(skynetGroups, iads.samSites[i]) end iads.earlyWarningRadars = {} iads.samSites = {} for i = 1, #skynetGroups do local skynetGroup = skynetGroups[i] setGroupState(skynetGroup) end ]] end function veafSkynet.deactivateNetworkOfCoalition(iCoalitionId, elementStates) local veafSkynetNetwork = veafSkynet.getNetwork(veafSkynet.defaultIADS[tostring(iCoalitionId)]) veafSkynet.deactivateNetwork(veafSkynetNetwork, elementStates) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Load module ------------------------------------------------------------------------------------------------------------------------------------------------------------- veaf.loggers.get(veafSkynet.Id):info(string.format("Loading version %s", veafSkynet.Version)) ------------------ END script veafSkynetIadsHelper.lua ------------------ ------------------ START script veafSkynetIadsMonitor.lua ------------------ ------------------------------------------------------------------ -- VEAF Skynet-IADS contacts monitoring -- By Flogas (2023) -- -- Features: -- --------- -- * This module offers tools to trigger actions when a Skynet IADS detects a target -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafSkynetMonitor = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafSkynetMonitor.Id = "SKYNET_MONITOR" --- Version. veafSkynetMonitor.Version = "1.1.0" -- trace level, specific to this module --veafSkynetMonitor.LogLevel = "trace" veaf.loggers.new(veafSkynetMonitor.Id, veafSkynetMonitor.LogLevel) --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- general tools --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- local function tableContains(tab, element) for _, e in pairs(tab) do if e == element then return true end end return false end local function tableRemove(tab, element) for i, e in pairs(tab) do if e == element then table.remove(tab, i) return end end end local function isNullOrEmpty(s) return (s == nil or (type(s) == "string" and s == "")) end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- VeafSkynetMonitorDescriptor class --- Provides text descriptions to check an IADS content and structure --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- VeafSkynetMonitorDescriptor = {} VeafSkynetMonitorDescriptor._staticSring = { Separator = " | ", Indentation = " " } VeafSkynetMonitorDescriptor.Option = { -- general Ewr = 1, -- display Early Warning Radars Sam = 2, -- display SAM sites Targets = 3, -- display targets tracked by the IADS -- Elements (Sam sites, EWRs) ElementTargets = 4, -- display targets tracked by each sam site ElementDetail = 5, -- display static element informations (go live conditions, HARM detection percentages, etc) ElementStructure = 6, -- display units attached to each element (point defences, child radars, etc) NestedPointDefences = 7 -- the point defences will be displayed as part of the element they defend } --------------------------------------------------------------------------------------------------- --- CTOR function VeafSkynetMonitorDescriptor:Create(iads, options) options = options or { VeafSkynetMonitorDescriptor.Option.Ewr, VeafSkynetMonitorDescriptor.Option.Sam, VeafSkynetMonitorDescriptor.Option.Targets } local this = { Iads = iads, Options = options, FilterNatoName = {} } setmetatable(this, self) self.__index = self return this end function VeafSkynetMonitorDescriptor:GetIndentationString(iIndentation) iIndentation = iIndentation or 0 return string.rep(self._staticSring.Indentation, iIndentation) end function VeafSkynetMonitorDescriptor:AppendString(s, sAppend) sAppend = sAppend or "" if (isNullOrEmpty(s)) then return sAppend elseif (isNullOrEmpty(sAppend)) then return s else return s .. self._staticSring.Separator .. sAppend end end function VeafSkynetMonitorDescriptor:NewLine(s, iIndentation) return s .. "\n" .. self:GetIndentationString(iIndentation) end function VeafSkynetMonitorDescriptor:AppendLine(s, sAppend, iIndentation) sAppend = sAppend or "" if (isNullOrEmpty(s)) then return self:GetIndentationString(iIndentation) .. sAppend elseif (isNullOrEmpty(sAppend)) then return s else return self:NewLine(s, iIndentation) .. sAppend end end function VeafSkynetMonitorDescriptor:GetStringSkynetElement(skynetElement) local s = veafSkynet.getStringSkynetElement(skynetElement) local dcsGroup = veafSkynet.getDcsGroupFromSkynetElement(skynetElement) if (dcsGroup == nil) then s = s .. " (dcs group not found)" else local bActive = false for _, dcsUnit in pairs(dcsGroup:getUnits()) do if (dcsUnit:isActive()) then bActive = true break end end if (not bActive) then s = s .. " (dcs group not active)" end end return s end function VeafSkynetMonitorDescriptor:GetStringElementStructure(details, sDetailType) local s = "" if (details == nil or #details <= 0) then s = self:AppendString(s, "No " .. sDetailType) else local iMaximumRangeMeters = nil for i = 1, #details do local detail = details[i] if (detail.maximumRange and (iMaximumRangeMeters == nil or iMaximumRangeMeters < detail.maximumRange))then iMaximumRangeMeters = detail.maximumRange end end local sMaximumRange = "" if (iMaximumRangeMeters) then iMaximumRangeMeters = veaf.round(mist.utils.metersToNM(iMaximumRangeMeters), 1) sMaximumRange = " range:" .. iMaximumRangeMeters .. "nm" end s = self:AppendString(s, sDetailType .. ":" .. #details .. sMaximumRange) end return s end function VeafSkynetMonitorDescriptor:GetStringEwr(ewr, iIndentation) local s = "" -- Site description s = self:AppendLine(s, "EWR : " .. VeafSkynetMonitorDescriptor:GetStringSkynetElement(ewr), iIndentation) -- Site detail if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementDetail)) then s = self:NewLine(s, iIndentation + 1) if (ewr.harmDetectionChance) then s = self:AppendString(s, "HARM detection " .. ewr.harmDetectionChance .. "%") end if (ewr.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI) then s = self:AppendString(s, "Autonomous:DCS AI") elseif (ewr.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) then s = self:AppendString(s, "Autonomous:dark") end if (ewr.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE) then s = self:AppendString(s, "Go live:kill zone") elseif (ewr.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) then s = self:AppendString(s, "Go live:search range") end end -- Site structure local pointDefences = ewr:getPointDefences() if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementStructure)) then local radars = ewr:getRadars() s = self:NewLine(s, iIndentation + 1) s = self:AppendString(s, self:GetStringElementStructure(radars, "Radars")) s = self:AppendString(s, self:GetStringElementStructure(pointDefences, "Point defences")) end -- Site state s = self:NewLine(s, iIndentation + 1) if (ewr:isActive()) then s = self:AppendString(s, "Active") else s = self:AppendString(s, "Not active") end if (ewr:getAutonomousState()) then s = self:AppendString(s, "Autonomous") end if (ewr.harmSilenceID ~= nil) then s = self:AppendString(s, "Defending HARM (shutdown time " .. ewr.harmShutdownTime .. "s)") end if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementTargets)) then local targets = ewr:getDetectedTargets() if (targets == nil or #targets <= 0) then s = self:AppendLine(s, "No targets", iIndentation + 1) else s = self:AppendLine(s, "Targets :", iIndentation + 1) for i = 1, #targets do local target = targets[i] s = self:AppendLine(s, target:getName(), iIndentation + 2) s = self:AppendString(s, target:getTypeName()) end end end if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.NestedPointDefences)) then if (pointDefences and #pointDefences > 0) then s = self:AppendLine(s, "Point defences :", iIndentation + 1) for i = 1, #pointDefences do local pointDefence = pointDefences[i] s = self:AppendLine(s, self:GetStringSam(pointDefence, iIndentation + 2)) end end end return s end function VeafSkynetMonitorDescriptor:GetStringSam(samSite, iIndentation) local s = "" -- Site description s = self:AppendLine(s, "Sam site : " .. VeafSkynetMonitorDescriptor:GetStringSkynetElement(samSite), iIndentation) if (samSite.isAPointDefence) then s = self:AppendString(s, "**Point defence**") end -- Site detail if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementDetail)) then s = self:NewLine(s, iIndentation + 1) if (samSite:getCanEngageAirWeapons()) then s = self:AppendString(s, "Can engage air weapons") end if (samSite:getCanEngageHARM()) then s = self:AppendString(s, "Can engage HARMs") end if (samSite.harmDetectionChance) then s = self:AppendString(s, "HARM detection " .. samSite.harmDetectionChance .. "%") end if (samSite.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI) then s = self:AppendString(s, "Autonomous:DCS AI") elseif (samSite.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) then s = self:AppendString(s, "Autonomous:dark") end if (samSite.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE) then s = self:AppendString(s, "Go live:kill zone " .. samSite.firingRangePercent .. "% of max range") elseif (samSite.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) then s = self:AppendString(s, "Go live:search range") end end local iRangeMeters = 0 local launchers = samSite:getLaunchers() -- Site structure local pointDefences = samSite:getPointDefences() if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementStructure)) then local searchRadars = samSite:getSearchRadars() local trackRadars = samSite:getTrackingRadars() local launchers = samSite:getLaunchers() s = self:NewLine(s, iIndentation + 1) s = self:AppendString(s, self:GetStringElementStructure(searchRadars, "Search radars")) s = self:AppendString(s, self:GetStringElementStructure(trackRadars, "Track radars")) s = self:AppendString(s, self:GetStringElementStructure(launchers, "Launchers")) s = self:AppendString(s, self:GetStringElementStructure(pointDefences, "Point defences")) end -- Site state s = self:NewLine(s, iIndentation + 1) if (samSite:isActive()) then s = self:AppendString(s, "Active") else s = self:AppendString(s, "Not active") end if (samSite:getAutonomousState()) then s = self:AppendString(s, "Autonomous") end if (samSite:getActAsEW()) then s = self:AppendString(s, "Acting as EW") end if (not samSite:hasRemainingAmmo()) then s = self:AppendString(s, "No ammo") end if (samSite.harmSilenceID ~= nil) then s = self:AppendString(s, "Defending HARM (shutdown time " .. samSite.harmShutdownTime .. "s)") end if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementTargets)) then local targets = samSite:getDetectedTargets() if (targets == nil or #targets <= 0) then s = self:AppendLine(s, "No targets", iIndentation + 1) else s = self:AppendLine(s, "Targets :", iIndentation + 1) for i = 1, #targets do local target = targets[i] s = self:AppendLine(s, target:getName(), iIndentation + 2) s = self:AppendString(s, target:getTypeName()) end end end if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.NestedPointDefences)) then if (pointDefences and #pointDefences > 0) then s = self:AppendLine(s, "Point defences :", iIndentation + 1) for i = 1, #pointDefences do local pointDefence = pointDefences[i] s = self:AppendLine(s, self:GetStringSam(pointDefence, iIndentation + 2)) end end end return s end function VeafSkynetMonitorDescriptor:GetStringDescription() local s = "" s = self:AppendLine("IADS : " .. self.Iads:getCoalitionString()) local iIndentation = 1 if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.Ewr)) then local ewrs = self.Iads:getEarlyWarningRadars() if (#ewrs <= 0) then s = self:AppendLine(s, "No EWR", iIndentation) else s = self:AppendLine(s, "EWRs :", iIndentation) for i = 1, #ewrs do local ewr = ewrs[i] local bAddEwr = true if(self.FilterNatoName and #self.FilterNatoName > 0 and not tableContains(self.FilterNatoName, ewr:getNatoName())) then bAddEwr = false end if (bAddEwr) then s = self:AppendLine(s, self:GetStringEwr(ewr, iIndentation + 1)) end end end end if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.Sam)) then local samSites = self.Iads:getSAMSites() if (#samSites <= 0) then s = self:AppendLine(s, "No sam sites", iIndentation) else s = self:AppendLine(s, "Sam sites :", iIndentation) for i = 1, #samSites do local samSite = samSites[i] local bAddSamSite = true if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.NestedPointDefences) and samSite.isAPointDefence) then bAddSamSite = false end if(self.FilterNatoName and #self.FilterNatoName > 0 and not tableContains(self.FilterNatoName, samSite:getNatoName())) then bAddSamSite = false end if (bAddSamSite) then s = self:AppendLine(s, self:GetStringSam(samSite, iIndentation + 1)) end end end end if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.Targets)) then end return s end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- VeafSkynetMonitorTask base class --- Named monitoring task --- A monitoring task represents a task to be executed by the monitoring thread --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- VeafSkynetMonitorTask = {} --------------------------------------------------------------------------------------------------- --- CTOR function VeafSkynetMonitorTask:Create(sName) local this = { Name = sName } setmetatable(this, self) self.__index = self return this end function VeafSkynetMonitorTask:ToString() return self.Name end function VeafSkynetMonitorTask:Execute() veaf.loggers.get(veafSkynetMonitor.Id):trace("Executing VeafSkynetMonitorTask") end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- VeafSkynetMonitorTaskContacts class --- Named monitoring task to check for contacts detected and lost by an iads --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- VeafSkynetMonitorTaskContacts = {} VeafSkynetMonitorTaskContacts = inheritsFrom(VeafSkynetMonitorTask) -- oop functions are from Skynet --------------------------------------------------------------------------------------------------- --- CTOR function VeafSkynetMonitorTaskContacts:Create(sName, iads, unitsToMonitor, onDetectedAction, onLostAction) local this = self:superClass():Create(sName, iads) setmetatable(this, self) self.__index = self this.Iads = iads this.UnitsToMonitor = unitsToMonitor this.TrackedUnits = {} this.OnDetectedAction = onDetectedAction this.OnLostAction = onLostAction return this end function VeafSkynetMonitorTaskContacts:ToString() return self.Name .. " | IADS [" .. self.Iads:getCoalitionString() .. "]" end function VeafSkynetMonitorTaskContacts:Execute() veaf.loggers.get(veafSkynetMonitor.Id):trace("Executing VeafSkynetMonitorTaskContacts") local currentContacts = self:GetIadsCurrentContacts() veaf.loggers.get(veafSkynetMonitor.Id):trace(self:ToString() .. " - currently tracking " .. #self.TrackedUnits .. " monitored units") for _, sContactName in pairs(currentContacts) do if (self:ContactIsToMonitor(sContactName) and not self:ContactIsTracked(sContactName)) then veaf.loggers.get(veafSkynetMonitor.Id):trace("Monitored contact detected: " .. sContactName) self:AddTrackedContact(sContactName) local err, errmsg = pcall(self.OnDetectedAction, sContactName) end end for _, sContactName in pairs(self.TrackedUnits) do if (not tableContains(currentContacts, sContactName)) then veaf.loggers.get(veafSkynetMonitor.Id):trace("Monitored contact lost: " .. sContactName) self:RemoveTrackedContact(sContactName) local err, errmsg = pcall(self.OnLostAction, sContactName) end end end function VeafSkynetMonitorTaskContacts:ContactIsToMonitor(sContactName) return self.UnitsToMonitor and tableContains(self.UnitsToMonitor, sContactName) end function VeafSkynetMonitorTaskContacts:GetIadsCurrentContacts() local currentContacts = {} for _, contact in pairs(self.Iads:getContacts()) do table.insert(currentContacts, contact:getName()) end return currentContacts end function VeafSkynetMonitorTaskContacts:ContactIsTracked(sContactName) return tableContains(self.TrackedUnits, sContactName) end function VeafSkynetMonitorTaskContacts:AddTrackedContact(sContactName) table.insert(self.TrackedUnits, sContactName) end function VeafSkynetMonitorTaskContacts:RemoveTrackedContact(sContactName) tableRemove(self.TrackedUnits, sContactName) end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- VeafSkynetMonitorTaskDescriptor class --- Named monitoring task to display informations regarding an iads --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- VeafSkynetMonitorTaskDescriptor = {} VeafSkynetMonitorTaskDescriptor = inheritsFrom(VeafSkynetMonitorTask) -- oop functions are from Skynet VeafSkynetMonitorTaskDescriptor.OutputDcsTextRecipients = { All = "*All*", Coalition = "*Coalition*" } --------------------------------------------------------------------------------------------------- --- CTOR function VeafSkynetMonitorTaskDescriptor:Create(sName, descriptors, sOutputLogLevel, outputDcsTextRecipients) local this = self:superClass():Create(sName) setmetatable(this, self) self.__index = self this.Descriptors = descriptors this.OutputLogLevel = sOutputLogLevel this.OutputDcsTextRecipients = outputDcsTextRecipients return this end function VeafSkynetMonitorTaskDescriptor:Output(sInformation) if (self.OutputLogLevel == "error") then veaf.loggers.get(veafSkynetMonitor.Id):error(sInformation) elseif (self.OutputLogLevel == "warning") then veaf.loggers.get(veafSkynetMonitor.Id):warning(sInformation) elseif (self.OutputLogLevel == "info") then veaf.loggers.get(veafSkynetMonitor.Id):info(sInformation) elseif (self.OutputLogLevel == "debug") then veaf.loggers.get(veafSkynetMonitor.Id):debug(sInformation) elseif (self.OutputLogLevel == "trace") then veaf.loggers.get(veafSkynetMonitor.Id):trace(sInformation) end if (self.OutputDcsTextRecipients and type(self.OutputDcsTextRecipients) == "table" and #self.OutputDcsTextRecipients > 0) then local iDuration = 4 for _, recipient in pairs(self.OutputDcsTextRecipients) do if (recipient == VeafSkynetMonitorTaskDescriptor.OutputDcsTextRecipients.All) then trigger.action.outText(sInformation, iDuration) elseif (tableContains (coalition.side, recipient)) then trigger.action.outTextForCoalition(recipient, sInformation, iDuration) else local group = Group.getByName(recipient) if (group) then trigger.action.outTextForGroup(group.id_, sInformation, iDuration) end end end end end function VeafSkynetMonitorTaskDescriptor:Execute() veaf.loggers.get(veafSkynetMonitor.Id):trace("Executing VeafSkynetMonitorTaskInformations") self:Output("----- Task Descriptor [ " .. self.Name .. " ] -----") for _, descriptor in pairs(self.Descriptors) do local sInformation = descriptor:GetStringDescription() self:Output(sInformation) end end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- Skynet monitoring thread management --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veafSkynetMonitor._monitoringTasks = {} veafSkynetMonitor._monitoringThreadId = nil veafSkynetMonitor._interval = 5 -- iads.contactUpdateInterval function veafSkynetMonitor.AddMonitoringTask(task) if (task == nil) then return end if (task.Name == nil or task.Name == "") then veaf.loggers.get(veafSkynetMonitor.Id):error("Monitoring task with incorrect name cannot be added") return end if (veafSkynetMonitor._monitoringTasks[task.Name] ~= nil) then veaf.loggers.get(veafSkynetMonitor.Id):error("Monitoring task with name [" .. task.Name .. "] already added") return end veaf.loggers.get(veafSkynetMonitor.Id):trace("Adding monitoring task: " .. task:ToString()) veafSkynetMonitor._monitoringTasks[task.Name] = task if (veafSkynetMonitor._monitoringThreadId == nil) then veaf.loggers.get(veafSkynetMonitor.Id):trace("Starting mist thread") veafSkynetMonitor._monitoringThreadId = mist.scheduleFunction(veafSkynetMonitor.ExecuteMonitoringTasks, {}, timer.getTime() + veafSkynetMonitor._interval, veafSkynetMonitor._interval, timer.getTime() + 3600) end end function veafSkynetMonitor.AddMonitoringTaskContacts(sTaskName, iads, unitsToMonitor, onDetectedAction, onLostAction) local task = VeafSkynetMonitorTaskContacts:Create(sTaskName, iads, unitsToMonitor, onDetectedAction, onLostAction) veafSkynetMonitor.AddMonitoringTask(task) end function veafSkynetMonitor.RemoveMonitoringTask(sTaskName) sTaskName = sTaskName or "" if (sTaskName ~= "" and veafSkynetMonitor._monitoringTasks[sTaskName]) then veaf.loggers.get(veafSkynetMonitor.Id):trace("Removing monitoring task: " .. veafSkynetMonitor._monitoringTasks[sTaskName]:ToString()) veafSkynetMonitor._monitoringTasks[sTaskName] = nil end if (veaf.length(veafSkynetMonitor._monitoringTasks) <= 0) then veaf.loggers.get(veafSkynetMonitor.Id):trace("Nothing to monitor, stopping mist thread") mist.removeFunction(veafSkynetMonitor._monitoringThreadId) end end function veafSkynetMonitor.ExecuteMonitoringTasks() for _, task in pairs(veafSkynetMonitor._monitoringTasks) do task:Execute() end end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- Skynet iads informations --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veaf.loggers.get(veafSkynetMonitor.Id):info(string.format("Loading version %s", veafSkynetMonitor.Version)) ------------------ END script veafSkynetIadsMonitor.lua ------------------ ------------------ START script veafTime.lua ------------------ ------------------------------------------------------------------ -- VEAF time and date tools -- By Flogas (2024) -- -- Features: -- --------- -- * Provides a suite of tools to manage date and time information relative to the DCS mission -- standard lua datetime object is used when appropriate: -- --> as returned by os.date("*t", 906000490) -- --> dateTime = { year = 1998, month = 9, day = 16, yday = 259, wday = *unused*, hour = 23, min = 48, sec = 10, isdst = *unused* } ------------------------------------------------------------------ veafTime = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global module settings ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafTime.Id = "TIME" --- Version. veafTime.Version = "1.1.3" -- trace level, specific to this module --veafTime.LogLevel = "trace" veaf.loggers.new(veafTime.Id, veafTime.LogLevel) ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Local constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- local _iSecondsInMinute = 60 local _iSecondsInHour = 3600 local _iSecondsInDay = 86400 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Local tools ------------------------------------------------------------------------------------------------------------------------------------------------------------- local function _isLeapYear(iYear) return iYear % 4 == 0 and (iYear % 100 ~= 0 or iYear % 400 == 0) end local function _getDaysInMonth(iMonth, iYear) local daysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} if iMonth == 2 and _isLeapYear(iYear) then return 29 end return daysInMonth[iMonth] end local function _getDayOfYear(iDay, iMonth, iYear) local iDayOfYear = iDay for i = 1, iMonth - 1 do iDayOfYear = iDayOfYear + _getDaysInMonth(i, iYear) end return iDayOfYear end local function _adjustDate(dateTime, iDaysOffset) local iYear = dateTime.year local iMonth = dateTime.month local iDay = dateTime.day local iDayOfYear = dateTime.yday -- Handle day changes iDay = iDay + iDaysOffset iDayOfYear = iDayOfYear + iDaysOffset -- Handle forward changes while true do local iDaysInMonth = _getDaysInMonth(iMonth, iYear) if (iDay <= iDaysInMonth) then break end iDay = iDay - iDaysInMonth iMonth = iMonth + 1 if (iMonth > 12) then iMonth = 1 iYear = iYear + 1 -- Reset yday for new year iDayOfYear = iDay end end -- Handle backward changes while (iDay <= 0) do iMonth = iMonth - 1 if (iMonth <= 0) then iMonth = 12 iYear = iYear - 1 end iDay = iDay + _getDaysInMonth(iMonth, iYear) -- Adjust yday for previous year if (iMonth == 12) then iDayOfYear = 365 + (_isLeapYear(iYear) and 1 or 0) + iDay - _getDaysInMonth(iMonth, iYear) end end return { year = iYear, month = iMonth, day = iDay, yday = iDayOfYear, hour = dateTime.hour, min = dateTime.min, sec = dateTime.sec } end local function _decimalToHoursMinutes(iDecimalHours) -- Handle negative hours (can happen with UTC times) if (iDecimalHours < 0) then iDecimalHours = iDecimalHours + 24 end -- Handle hours >= 24 iDecimalHours = iDecimalHours % 24 local iHours = math.floor(iDecimalHours) local iMinutes = math.floor((iDecimalHours - iHours) * 60) return iHours, iMinutes end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- General date and time tools ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafTime.getMissionDateTime(iAbsTime) iAbsTime = iAbsTime or timer.getAbsTime() -- Calculate hours, minutes, and remaining seconds local iDay = env.mission.date.Day local iMonth = env.mission.date.Month local iYear = env.mission.date.Year local iHour = math.floor(iAbsTime / _iSecondsInHour) local iRemainingSeconds = iAbsTime % _iSecondsInHour local iMinute = math.floor(iRemainingSeconds / _iSecondsInMinute) local iSecond = iRemainingSeconds % _iSecondsInMinute -- Handle day rollover local iAdditionalDays = math.floor(iHour / 24) iHour = iHour % 24 -- Add the additional days iDay = iDay + iAdditionalDays -- Handle month and year rollover while true do local iDaysInCurrentMonth = _getDaysInMonth(iMonth, iYear) if (iDay <= iDaysInCurrentMonth) then break end iDay = iDay - iDaysInCurrentMonth iMonth = iMonth + 1 if iMonth > 12 then iMonth = 1 iYear = iYear + 1 end end local idayOfYear = _getDayOfYear(iDay, iMonth, iYear) return { year = iYear, month = iMonth, day = iDay, yday = idayOfYear, hour = iHour, min = iMinute, sec = iSecond } end function veafTime.getMissionAbsTime(dateTime) local iMissionStartDay = env.mission.date.Day local iMissionStartMonth = env.mission.date.Month local iMissionStartYear = env.mission.date.Year local iDay = dateTime.day local iMonth = dateTime.month local iYear = dateTime.year local iHour = dateTime.hour local iMinute = dateTime.min local iSecond = dateTime.sec local iTotalDays = 0 -- If we're in the same year if iMissionStartYear == iYear then iTotalDays = _getDayOfYear(iDay, iMonth, iYear) - _getDayOfYear(iMissionStartDay, iMissionStartMonth, iMissionStartYear) else -- Days remaining in the first year iTotalDays = (_isLeapYear(iMissionStartYear) and 366 or 365) - _getDayOfYear(iMissionStartDay, iMissionStartMonth, iMissionStartYear) -- Add days for full years in between for iYear = iMissionStartYear + 1, iYear - 1 do iTotalDays = iTotalDays + (_isLeapYear(iYear) and 366 or 365) end -- Add days in the final year iTotalDays = iTotalDays + _getDayOfYear(iDay, iMonth, iYear) end -- Calculate total seconds local iAbsTime = iSecond + (iMinute * _iSecondsInMinute) + (iHour * _iSecondsInHour) + (iTotalDays * _iSecondsInDay) return iAbsTime end function veafTime.absTimeToDateTime(iAbsTime) iAbsTime = iAbsTime or timer.getAbsTime() return veafTime.getMissionDateTime(iAbsTime) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Date and time string display tools ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafTime.toStringDate(dateTime) return string.format("%02d/%02d/%d", dateTime.day, dateTime.month, dateTime.year) end function veafTime.absTimeToStringDate(iAbsTime) local dateTime = veafTime.absTimeToDateTime(iAbsTime) return veafTime.toStringDate(dateTime) end function veafTime.toStringTime(dateTime, bWithSeconds) if (bWithSeconds == nil) then bWithSeconds = true end --veaf.loggers.get(veafTime.Id):trace(veaf.p(dateTime)) if (bWithSeconds) then return string.format("%02d:%02d:%02d", dateTime.hour, dateTime.min, dateTime.sec) else return string.format("%02d:%02d", dateTime.hour, dateTime.min) end end function veafTime.absTimeToStringTime(iAbsTime, bWithSeconds) local dateTime = veafTime.absTimeToDateTime(iAbsTime) return veafTime.toStringTime(dateTime, bWithSeconds) end function veafTime.toStringDateTime(dateTime, bWithSeconds) return string.format("%s %s", veafTime.toStringDate(dateTime), veafTime.toStringTime(dateTime, bWithSeconds)) end function veafTime.absTimeToStringDateTime(iAbsTime) local dateTime = veafTime.absTimeToDateTime(iAbsTime) return veafTime.toStringDateTime(dateTime, bWithSeconds) end -- Helper function to determine season based on latitude and month function veafTime.determineSeason(month, latitude) -- Determine if in Northern or Southern Hemisphere local isNorthernHemisphere = latitude == nil or latitude >= 0 -- Season mapping adjusted for hemisphere if isNorthernHemisphere then -- Northern Hemisphere seasons if month >= 3 and month <= 5 then return "spring" elseif month >= 6 and month <= 8 then return "summer" elseif month >= 9 and month <= 11 then return "autumn" else return "winter" end else -- Southern Hemisphere seasons (reversed) if month >= 3 and month <= 5 then return "autumn" elseif month >= 6 and month <= 8 then return "winter" elseif month >= 9 and month <= 11 then return "spring" else return "summer" end end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Timezones ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafTime.getTimezone(vec3) local nTimezoneOffset = 0 local sTheatre = env.mission.theatre if (vec3) then -- Try to approximate for the vec3 - each timezone is roughly 15 degrees wide local nLatitude, nLongitude, _ = coord.LOtoLL(vec3) nTimezoneOffset = math.floor((nLongitude + 7.5) / 15) elseif (sTheatre == veaf.theatreName.Caucasus) then nTimezoneOffset = 4 elseif (sTheatre == veaf.theatreName.PersianGulf) then nTimezoneOffset = 4 elseif (sTheatre == veaf.theatreName.Nevada) then nTimezoneOffset = -8 -- Nevada uses DST (UTC-7 march through october) but it is not modeled in DCS elseif (sTheatre == veaf.theatreName.Normandy) then nTimezoneOffset = 0 elseif (sTheatre == veaf.theatreName.TheChannel) then nTimezoneOffset = 2 elseif (sTheatre == veaf.theatreName.Syria) then nTimezoneOffset = 3 elseif (sTheatre == veaf.theatreName.MarianaIslands) then nTimezoneOffset = 10 elseif (sTheatre == veaf.theatreName.Falklands) then nTimezoneOffset = -3 elseif (sTheatre == veaf.theatreName.Sinai) then nTimezoneOffset = 2 elseif (sTheatre == veaf.theatreName.Kola) then nTimezoneOffset = 3 elseif (sTheatre == veaf.theatreName.Afghanistan) then nTimezoneOffset = 4.5 end --veaf.loggers.get(veafTime.Id):trace(string.format("%s - timezone=%f", env.mission.theatre, nTimezoneOffset)) return nTimezoneOffset end function veafTime.toZulu(dateTime, nOffsetHours) nOffsetHours = nOffsetHours or veafTime.getTimezone() -- Create a new table to avoid modifying the original local dateTimeZulu = {} for k, v in pairs(dateTime) do dateTimeZulu[k] = v end -- Convert hours and handle day boundary changes local iTotalMinutes = dateTimeZulu.hour * 60 + dateTimeZulu.min - (nOffsetHours * 60) local iDays = math.floor(iTotalMinutes / (24 * 60)) iTotalMinutes = iTotalMinutes % (24 * 60) -- Update hours and minutes dateTimeZulu.hour = math.floor(iTotalMinutes / 60) dateTimeZulu.min = iTotalMinutes % 60 -- Handle date changes if needed if iDays ~= 0 then return _adjustDate(dateTimeZulu, iDays) end return dateTimeZulu end function veafTime.toLocal(utcDateTime, nOffsetHours) nOffsetHours = nOffsetHours or veafTime.getTimezone() return veafTime.toZulu(utcDateTime, -nOffsetHours) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Sun times ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafTime.getSunTimesZulu(vec3, iAbsTime) local nLatitude, nLongitude, _ = coord.LOtoLL(vec3) local date = veafTime.absTimeToDateTime(iAbsTime) return veafSunTimes.getSunTimes(nLatitude, nLongitude, date, 0) end function veafTime.getSunTimesLocal(vec3, iAbsTime) local nLatitude, nLongitude, _ = coord.LOtoLL(vec3) local date = veafTime.absTimeToDateTime(iAbsTime) return veafSunTimes.getSunTimes(nLatitude, nLongitude, date, veafTime.getTimezone()) end function veafTime.isAeronauticalNight(vec3, iAbsTime) local dateTime = veafTime.absTimeToDateTime(iAbsTime) local sunTimes = veafTime.getSunTimesLocal(vec3, iAbsTime) local sunriseTime = sunTimes.Sunrise local sunsetTime = sunTimes.Sunset --veaf.loggers.get(veafTime.Id):trace(veaf.p(sunriseTime)) --veaf.loggers.get(veafTime.Id):trace(veaf.p(sunsetTime)) local iCurrentSeconds = dateTime.hour * _iSecondsInHour + dateTime.min * _iSecondsInMinute + dateTime.sec local iSunriseSeconds = sunriseTime.hour * _iSecondsInHour + sunriseTime.min * _iSecondsInMinute + sunriseTime.sec - (30 * _iSecondsInMinute) -- sunrise - 30 min local iSunsetSeconds = sunsetTime.hour * _iSecondsInHour + sunsetTime.min * _iSecondsInMinute + sunsetTime.sec + (30 * _iSecondsInMinute) -- sunset + 30 min --veaf.loggers.get(veafTime.Id):trace(string.format("iCurrentSeconds=%d iSunriseSeconds=%d iSunsetSeconds=%d", iCurrentSeconds, iSunriseSeconds, iSunsetSeconds)) return iCurrentSeconds < iSunriseSeconds or iCurrentSeconds > iSunsetSeconds end local dawnAngle, duskAngle = 6, 6 local DR = math.pi / 180 local K1 = 15 * math.pi * 1.0027379 / 180 veafSunTimes = {} veafSunTimes.__index = veafSunTimes --------------------------------------------------------------------------------------------------- --- CTOR function veafSunTimes:create() local this = { sunRiseSetTimes = {}, moonRiseSetTimes = {}, NoSunRise = false, NoSunSet = false, lat = 0, long = 0, timeOffset = 0, jDateSun = nil } setmetatable(this, veafSunTimes) return this end ---------------------------------------------------------------------------------------------------- -- Main static method ---------------------------------------------------------------------------------------------------- function veafSunTimes.getSunTimes(nLatitude, nLongitude, date, nTimeZone) local sunTimes = veafSunTimes:create() sunTimes:compute(nLatitude, nLongitude, nTimeZone, date) local sunrise = sunTimes.sunRiseSetTimes[2] local sunset = sunTimes.sunRiseSetTimes[3] local iSunriseHour, iSunriseMinute = _decimalToHoursMinutes(sunrise) local iSunsetHour, iSunsetMinute = _decimalToHoursMinutes(sunset) return { Sunrise = { year = date.year, month = date.month, day = date.day, yday = date.yday, hour = iSunriseHour, min = iSunriseMinute, sec = 0 }, Sunset = { year = date.year, month = date.month, day = date.day, yday = date.yday, hour = iSunsetHour, min = iSunsetMinute, sec = 0 } } end ---------------------------------------------------------------------------------------------------- -- Main compute method ---------------------------------------------------------------------------------------------------- function veafSunTimes:compute(nLatitude, nLongitude, nTimeZone, date) self.sunRiseSetTimes = {6, 6, 6, 12, 13, 18, 18, 18, 24} self.moonRiseSetTimes = {0, 23.9} self.NoSunRise = false self.NoSunSet = false self.lat = nLatitude self.long = nLongitude self.timeOffset = nTimeZone self.jDateSun = self:julian(date.year, date.month, date.day) - (nLongitude / (15 * 24)) -- sun time calculations self:calcSunRiseSet() if self.NoSunRise or self.NoSunSet then -- adjust times to solar noon self.sunRiseSetTimes[2] = (self.sunRiseSetTimes[2] - 12) if self.NoSunRise then self.sunRiseSetTimes[3] = self.sunRiseSetTimes[2] + 0.0001 else self.sunRiseSetTimes[3] = (self.sunRiseSetTimes[2] - 0.0001) end self.sunRiseSetTimes[1] = 0 self.sunRiseSetTimes[4] = 0 end -- debugging -- print("dawn = " .. TimeString(sunRiseSetTimes[1], nTimeLZero, nTimeStyle) .. " (" .. sunRiseSetTimes[1] ..")") -- print("sunrise = " .. TimeString(sunRiseSetTimes[2], nTimeLZero, nTimeStyle) .. " (" .. sunRiseSetTimes[2] ..")") -- print("sunset = " .. TimeString(sunRiseSetTimes[3], nTimeLZero, nTimeStyle) .. " (" .. sunRiseSetTimes[3] ..")") -- print("twilight = " .. TimeString(sunRiseSetTimes[4], nTimeLZero, nTimeStyle) .. " (" .. sunRiseSetTimes[4] ..")") end ---------------------------------------------------------------------------------------------------- -- Following code kept as close as possible to https://gist.github.com/eDave56/6dfae1b62c4cf743afe0ad61e300f091 ---------------------------------------------------------------------------------------------------- ------------------------------------ [ sun time calculations ] ------------------------------------- function veafSunTimes:midDay(Ftime) local eqt = self:sunPosition(self.jDateSun + Ftime, 0) local noon = veafSunTimes.DMath.fixHour(12 - eqt) return noon end -- function midDay function veafSunTimes:sunAngleTime(angle, Ftime, direction) -- -- time at which sun reaches a specific angle below horizon -- local decl = self:sunPosition(self.jDateSun + Ftime, 1) local noon = self:midDay(Ftime) local t = (-veafSunTimes.DMath.Msin(angle) - veafSunTimes.DMath.Msin(decl) * veafSunTimes.DMath.Msin(self.lat)) / (veafSunTimes.DMath.Mcos(decl) * veafSunTimes.DMath.Mcos(self.lat)) if t > 1 then -- the sun doesn't rise today self.NoSunRise = 1 return noon elseif t < -1 then -- the sun doesn't set today self.NoSunSet = 1 return noon end t = 1 / 15 * veafSunTimes.DMath.arccos(t) return noon + ((direction == "CCW") and -t or t) end -- function sunAngleTime function veafSunTimes:sunPosition(jd, Declination) -- -- compute declination angle of sun -- local D = jd - 2451545 local g = veafSunTimes.DMath.fixAngle(357.529 + 0.98560028 * D) local q = veafSunTimes.DMath.fixAngle(280.459 + 0.98564736 * D) local L = veafSunTimes.DMath.fixAngle(q + 1.915 * veafSunTimes.DMath.Msin(g) + 0.020 * veafSunTimes.DMath.Msin(2 * g)) local R = 1.00014 - 0.01671 * veafSunTimes.DMath.Mcos(g) - 0.00014 * veafSunTimes.DMath.Mcos(2 * g) local e = 23.439 - 0.00000036 * D local RA = veafSunTimes.DMath.arctan2(veafSunTimes.DMath.Mcos(e) * veafSunTimes.DMath.Msin(L), veafSunTimes.DMath.Mcos(L)) / 15 local eqt = q / 15 - veafSunTimes.DMath.fixHour(RA) local decl = veafSunTimes.DMath.arcsin(veafSunTimes.DMath.Msin(e) * veafSunTimes.DMath.Msin(L)) if Declination == 1 then return decl else return eqt end end -- function sunPosition function veafSunTimes:julian(year, month, day) -- -- convert Gregorian date to Julian day -- if (month <= 2) then year = year - 1 month = month + 12 end local A = math.floor(year / 100) local B = 2 - A + math.floor(A / 4) local JD = math.floor(365.25 * (year + 4716)) + math.floor(30.6001 * (month + 1)) + day + B - 1524.5 return JD end -- function julian function veafSunTimes:setTimes(sunRiseSetTimes) local Ftimes = self:dayPortion(sunRiseSetTimes) local dawn = self:sunAngleTime(dawnAngle, Ftimes[2], "CCW") local sunrise = self:sunAngleTime(self:riseSetAngle(), Ftimes[3], "CCW") local sunset = self:sunAngleTime(self:riseSetAngle(), Ftimes[8], "CW") local dusk = self:sunAngleTime(duskAngle, Ftimes[7], "CW") return {dawn, sunrise, sunset, dusk} end -- function setTimes function veafSunTimes:calcSunRiseSet() self.sunRiseSetTimes = self:setTimes(self.sunRiseSetTimes) return self:adjustTimes(self.sunRiseSetTimes) end function veafSunTimes:adjustTimes(sunRiseSetTimes) for i = 1, #sunRiseSetTimes do sunRiseSetTimes[i] = sunRiseSetTimes[i] + (self.timeOffset - self.long / 15) end sunRiseSetTimes = self:adjustHighLats(sunRiseSetTimes) return sunRiseSetTimes end -- function adjustTimes function veafSunTimes:riseSetAngle() -- -- sun angle for sunset/sunrise -- -- local angle = 0.0347 * math.sqrt( elv ) local angle = 0.0347 return 0.833 + angle end -- function riseSetAngle function veafSunTimes:adjustHighLats(sunRiseSetTimes) -- -- adjust times for higher latitudes -- local nightTime = self:timeDiff(sunRiseSetTimes[3], sunRiseSetTimes[2]) sunRiseSetTimes[1] = self:refineHLtimes(sunRiseSetTimes[1], sunRiseSetTimes[2], (dawnAngle), nightTime, "CCW") return sunRiseSetTimes end -- function adjustHighLats function veafSunTimes:refineHLtimes(Ftime, base, angle, night, direction) -- -- refine time for higher latitudes -- local portion = night / 2 local FtimeDiff = (direction == "CCW") and self:timeDiff(Ftime, base) or self:timeDiff(base, Ftime) if not ((Ftime * 2) > 2) or (FtimeDiff > portion) then Ftime = base + ((direction == "CCW") and -portion or portion) end return Ftime end -- function refineHLtimes function veafSunTimes:dayPortion(sunRiseSetTimes) -- -- convert hours to day portions -- for i = 1, #sunRiseSetTimes do sunRiseSetTimes[i] = sunRiseSetTimes[i] / 24 end return sunRiseSetTimes end -- function dayPortion function veafSunTimes:timeDiff(time1, time2) -- -- difference between two times -- return veafSunTimes.DMath.fixHour(time2 - time1) end -- function timeDiff ----------------------------------- [ moon time calaculations ] ------------------------------------ -- VEAF not retained ------------------------------------- [ other odds and sods ] -------------------------------------- -- VEAF not retained ---------------------------------------- [ math functions ] ---------------------------------------- function veafSunTimes.fix(a, b) a = a - b * (math.floor(a / b)) return (a < 0) and a + b or a end function veafSunTimes.dtr(d) return (d * math.pi) / 180 end function veafSunTimes.rtd(r) return (r * 180) / math.pi end veafSunTimes.DMath = { Msin = function(d) return math.sin(veafSunTimes.dtr(d)) end, Mcos = function(d) return math.cos(veafSunTimes.dtr(d)) end, Mtan = function(d) return math.tan(veafSunTimes.dtr(d)) end, arcsin = function(d) return veafSunTimes.rtd(math.asin(d)) end, arccos = function(d) return veafSunTimes.rtd(math.acos(d)) end, arctan = function(d) return veafSunTimes.rtd(math.atan(d)) end, arccot = function(x) return veafSunTimes.rtd(math.atan(1 / x)) end, arctan2 = function(y, x) return veafSunTimes.rtd(math.atan2(y, x)) end, fixAngle = function(a) return veafSunTimes.fix(a, 360) end, fixHour = function(a) return veafSunTimes.fix(a, 24) end } --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- -------------------- TEST STUFF-------------------------------------------------------------------- --[[ local _TEST_LOCATIONS = { ["BATUMI"] = {41.6167547, 41.6367455, 4}, ["DAMASCUS"] = {34.802075, 38.996815, 3}, ["SAIPAN"] = {15.0979, 145.6739, 10}, ["LAS VEGAS"] = {36.176, -115.137, -8}, ["DUBAI"] = {23.424076, 53.847818, 4} } local function _TEST_SUN_TIMES(sLocation, date, sExpectedRise, sExpectedSet) local nLatitude = _TEST_LOCATIONS[sLocation][1] local nLongitude = _TEST_LOCATIONS[sLocation][2] local iTimezoneOffset = veafTime.getTimezone() if (date == nil) then date = veafTime.absTimeToDateTime() sExpectedRise = "no data" sExpectedSet = "no data" else date = {day = date[1], month = date[2], year = date[3]} end local sunTimes = veafSunTimes.getSunTimes(nLatitude, nLongitude, date, iTimezoneOffset) local sunrise = sunTimes.Sunrise local sunset = sunTimes.Sunset veaf.loggers.get(veafTime.Id):trace("%s ; %02d/%02d/%d : sunrise=%02d:%02d, sunset=%02d:%02d ( expected rise=%s, set=%s )", sLocation, date.day, date.month, date.year, sunrise.hour, sunrise.min, sunset.hour, sunset.min, sExpectedRise, sExpectedSet) end veaf.loggers.get(veafTime.Id):trace("%s >> TESTS TESTS TESTS", veafTime.Id) local sTheatre = string.lower(env.mission.theatre) local sLoc if (sTheatre == "caucasus") then sLoc = "BATUMI" _TEST_SUN_TIMES(sLoc, 1, 1, 2024, "08:40", "17:53") _TEST_SUN_TIMES(sLoc, 1, 4, 2024, "06:54", "19:39") _TEST_SUN_TIMES(sLoc, 1, 7, 2024, "05:43", "20:51") _TEST_SUN_TIMES(sLoc, 1, 10, 2024, "07:11", "18:54") elseif (sTheatre == "persiangulf") then sLoc = "DUBAI" _TEST_SUN_TIMES(sLoc, {1, 1, 2024}, "07:06", "17:49") _TEST_SUN_TIMES(sLoc, {1, 4, 2024}, "06:16", "18:40") _TEST_SUN_TIMES(sLoc, {1, 7, 2024}, "05:42", "19:15") _TEST_SUN_TIMES(sLoc, {1, 10, 2024}, "06:16", "18:11") elseif (sTheatre == "nevada") then sLoc = "LAS VEGAS" _TEST_SUN_TIMES(sLoc) _TEST_SUN_TIMES(sLoc, {1, 1, 2024}, "06:50", "16:37") _TEST_SUN_TIMES(sLoc, {1, 4, 2024}, "06:24 (05:24 w/o DST)", "19:04 (18:04 w/o DST)") _TEST_SUN_TIMES(sLoc, {1, 7, 2024}, "05:25 (04:25 w/o DST)", "20:14 (19:14 w/o DST)") _TEST_SUN_TIMES(sLoc, {1, 10, 2024}, "06:43 (05:43 w/o DST)", "18:26 (17:25 w/o DST)") elseif (sTheatre == "normandy") then sLoc = "" elseif (sTheatre == "thechannel") then sLoc = "" elseif (sTheatre == "syria") then sLoc = "DAMASCUS" _TEST_SUN_TIMES(sLoc, {1, 1, 2024}, "07:38", "17:37") _TEST_SUN_TIMES(sLoc, {1, 4, 2024}, "06:22", "18:56") _TEST_SUN_TIMES(sLoc, {1, 7, 2024}, "05:28", "19:46") _TEST_SUN_TIMES(sLoc, {1, 10, 2024}, "06:29", "18:18") elseif (sTheatre == "marianaislands") then sLoc = "SAIPAN" _TEST_SUN_TIMES(sLoc, {1, 1, 2024}, "06:42", "17:57") _TEST_SUN_TIMES(sLoc, {1, 4, 2024}, "06:12", "18:29") _TEST_SUN_TIMES(sLoc, {1, 7, 2024}, "05:51", "18:51") _TEST_SUN_TIMES(sLoc, {1, 10, 2024}, "06:07", "18:06") elseif (sTheatre == "falklands") then sLoc = "" elseif (sTheatre == "sinaiMap") then sLoc = "" elseif (sTheatre == "kola") then sLoc = "" elseif (sTheatre == "afghanistan") then sLoc = "" end ]] ------------------ END script veafTime.lua ------------------ ------------------ START script veafTransportMission.lua ------------------ ------------------------------------------------------------------ -- VEAF transport mission command and functions for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Listen to marker change events and creates a transport training mission, with optional parameters -- * Possibilities : -- * - create a zone with cargo to pick up, another with friendly troops awaiting their cargo, and optionaly enemy units on the way -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafTransportMission = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafTransportMission.Id = "TRANSPORTMISSION" --- Version. veafTransportMission.Version = "1.8.0" -- trace level, specific to this module --veafTransportMission.LogLevel = "trace" veaf.loggers.new(veafTransportMission.Id, veafTransportMission.LogLevel) --- Key phrase to look for in the mark text which triggers the command. veafTransportMission.Keyphrase = "_transport" veafTransportMission.CargoTypes = {"ammo_cargo", "barrels_cargo", "m117_cargo", "oiltank_cargo", "uh1h_cargo" } --, "container_cargo", "fueltank_cargo" } --- Number of seconds between each check of the friendly group ADF loop function veafTransportMission.SecondsBetweenAdfLoops = 30 --- Number of seconds between each check of the friendly group watchdog function veafTransportMission.SecondsBetweenWatchdogChecks = 15 --- Number of seconds between each smoke request on the target veafTransportMission.SecondsBetweenSmokeRequests = 180 --- Number of seconds between each flare request on the target veafTransportMission.SecondsBetweenFlareRequests = 120 --- Name of the friendly group that waits for the cargo veafTransportMission.BlueGroupName = "Transport - Allied Group" --- Name of the cargo units veafTransportMission.BlueCargoName = "Cargo - Cargo unit" --- Name of the enemy group that defends the way to the friendlies veafTransportMission.RedDefenseGroupName = "Cargo - Enemy Air Defense Group" --- Name of the enemy group that blocades the friendlies veafTransportMission.RedBlocadeGroupName = "Cargo - Enemy Blocade Group" veafTransportMission.RadioMenuName = "TRANSPORT MISSION" veafTransportMission.AdfRadioSound = "l10n/DEFAULT/beacon.ogg" veafTransportMission.AdfFrequency = 550000 -- in hz veafTransportMission.AdfPower = 1000 -- in Watt veafTransportMission.DoRadioTransmission = false -- set to true when radio transmissions will work -- minimum authorized route distance ; missions shorter than this will not be authorized veafTransportMission.MinimumRouteDistance = 15000 -- 15 km -- size of the safe zone (no enemy group before this distance, in % of the total distance) veafTransportMission.SafeZoneDistance = 0.6 -- 60% -- size of the sqfe zone near drop zone (no enemy group after this distance from the drop zone) veafTransportMission.DropZoneSafeZoneDistance = 5000 -- 5 km -- an enemy group every xxx meters of the way (randomized) veafTransportMission.EnemyDefenseDistanceStep = 3000 -- enemies groups generated along the way are offset to xxx meters max (left or right, randomized) veafTransportMission.LeftOrRightMaxOffset = 1500 -- enemies groups generated along the way are offset to xxx meters min (left or right, randomized) veafTransportMission.LeftOrRightMinOffset = 500 -- enemies groups generated far from the way are offset to xxx meters max (left or right, randomized) veafTransportMission.LeftOrRightMaxFarOffset = 7000 -- enemies groups generated far from the way are offset to xxx meters min (left or right, randomized) veafTransportMission.LeftOrRightMinFarOffset = 3000 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Friendly group watchdog function id veafTransportMission.friendlyGroupAliveCheckTaskID = 'none' -- Friendly group ADF transmission loop function id veafTransportMission.friendlyGroupAdfLoopTaskID = 'none' --- Radio menus paths veafTransportMission.targetMarkersPath = nil veafTransportMission.targetInfoPath = nil veafTransportMission.rootPath = nil ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function executed when a mark has changed. This happens when text is entered or changed. function veafTransportMission.onEventMarkChange(eventPos, event) -- Check if marker has a text and the veafTransportMission.keyphrase keyphrase. if event.text ~= nil and event.text:lower():find(veafTransportMission.Keyphrase) then -- Analyse the mark point text and extract the keywords. local options = veafTransportMission.markTextAnalysis(event.text) if options then -- Check options commands if options.transportmission then -- check security if not veafSecurity.checkSecurity_L1(options.password) then return end -- create the mission veafTransportMission.generateTransportMission(eventPos, options.size, options.defense, options.blocade, options.from) end else -- None of the keywords matched. return end -- Delete old mark. veaf.loggers.get(veafTransportMission.Id):trace(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Analyse the mark text and extract keywords. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Extract keywords from mark text. function veafTransportMission.markTextAnalysis(text) -- Option parameters extracted from the mark text. local switch = {} switch.transportmission = false -- size ; number of cargo to be transported switch.size = 1 -- defense [1-5] : air defense cover on the way (1 = light, 5 = heavy) switch.defense = 0 -- blocade [1-5] : enemy blocade around the drop zone (1 = light, 5 = heavy) switch.blocade = 0 -- start position, named point switch.from = nil -- password switch.password = nil -- Check for correct keywords. if text:lower():find(veafTransportMission.Keyphrase) then switch.transportmission = true else return nil end -- keywords are split by "," local keywords = veaf.split(text, ",") for _, keyphrase in pairs(keywords) do -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. local str = veaf.breakString(veaf.trim(keyphrase), " ") local key = str[1] local val = str[2] if key:lower() == "password" then -- Unlock the command veaf.loggers.get(veafTransportMission.Id):debug(string.format("Keyword password", val)) switch.password = val end if switch.transportmission and key:lower() == "size" then -- Set size. veaf.loggers.get(veafTransportMission.Id):debug(string.format("Keyword size = %d", val)) local nVal = tonumber(val) if nVal <= 5 and nVal >= 1 then switch.size = nVal end end if switch.transportmission and key:lower() == "defense" then -- Set defense. veaf.loggers.get(veafTransportMission.Id):debug(string.format("Keyword defense = %d", val)) local nVal = tonumber(val) if nVal <= 5 and nVal >= 0 then switch.defense = nVal end end if switch.transportmission and key:lower() == "blocade" then -- Set blocade. veaf.loggers.get(veafTransportMission.Id):debug(string.format("Keyword blocade = %d", val)) local nVal = tonumber(val) if nVal <= 5 and nVal >= 0 then switch.blocade = nVal end end if switch.transportmission and key:lower() == "from" then -- Set armor. veaf.loggers.get(veafTransportMission.Id):debug(string.format("Keyword from = %s", val)) switch.from = val end end return switch end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- CAS target group generation and management ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafTransportMission.doRadioTransmission(groupName) veaf.loggers.get(veafTransportMission.Id):trace("doRadioTransmission("..groupName..")") local group = Group.getByName(groupName) if group then veaf.loggers.get(veafTransportMission.Id):trace("Group is transmitting") local averageGroupPosition = veaf.getAveragePosition(groupName) veaf.loggers.get(veafTransportMission.Id):trace("averageGroupPosition=" .. veaf.vecToString(averageGroupPosition)) trigger.action.radioTransmission(veafTransportMission.AdfRadioSound, averageGroupPosition, 0, false, veafTransportMission.AdfFrequency, veafTransportMission.AdfPower) end veafTransportMission.friendlyGroupAdfLoopTaskID = mist.scheduleFunction(veafTransportMission.doRadioTransmission, { groupName }, timer.getTime() + veafTransportMission.SecondsBetweenAdfLoops) end function veafTransportMission.generateFriendlyGroup(groupPosition) veafSpawn.doSpawnGroup(groupPosition, 0, "US infgroup", nil, "USA", 0, 0, 0, veafTransportMission.BlueGroupName, true, false, true, true) if veafTransportMission.DoRadioTransmission then veafTransportMission.doRadioTransmission(veafTransportMission.BlueGroupName) end end --- Generates an enemy defense group on the way to the drop zone --- defenseLevel = 1 : 3-7 soldiers, GAZ-3308 transport --- defenseLevel = 2 : 3-7 soldiers, BTR-80 APC --- defenseLevel = 3 : 3-7 soldiers, chance of BMP-1 IFV, chance of Igla manpad --- defenseLevel = 4 : 3-7 soldiers, big chance of BMP-1 IFV, big chance of Igla-S manpad, chance of ZU-23 on a truck --- defenseLevel = 5 : 3-7 soldiers, BMP-1 IFV, big chance of Igla-S manpad, chance of ZSU-23-4 Shilka function veafTransportMission.generateEnemyDefenseGroup(groupPosition, groupName, defenseLevel) local groupDefinition = { disposition = { h = 6, w = 6}, units = {}, description = groupName, groupName = groupName, } -- generate an infantry group local groupCount = math.random(3, 7) for _ = 1, groupCount do local rand = math.random(3) local unitType = nil if rand == 1 then unitType = 'Soldier RPG' elseif rand == 2 then unitType = 'Soldier AK' else unitType = 'Infantry AK' end table.insert(groupDefinition.units, { unitType }) end -- add a transport vehicle or an APC/IFV if defenseLevel > 4 or (defenseLevel > 3 and math.random(100) > 33) or (defenseLevel > 2 and math.random(100) > 66) then table.insert(groupDefinition.units, { "BMP-1", cell=11, random = true }) elseif defenseLevel > 1 then table.insert(groupDefinition.units, { "BTR-80", cell=11, random = true }) else table.insert(groupDefinition.units, { "GAZ-3308", cell=11, random = true }) end -- add manpads if needed if defenseLevel > 3 and math.random(100) > 33 then -- for defenseLevel = 4-5, spawn a modern Igla-S team table.insert(groupDefinition.units, { "SA-18 Igla-S comm", random = true }) table.insert(groupDefinition.units, { "SA-18 Igla-S manpad", random = true }) elseif defenseLevel > 2 and math.random(100) > 66 then -- for defenseLevel = 3, spawn an older Igla team table.insert(groupDefinition.units, { "SA-18 Igla comm", random = true }) table.insert(groupDefinition.units, { "SA-18 Igla manpad", random = true }) else -- for defenseLevel = 0, don't spawn any manpad end -- add an air defenseLevel vehicle if defenseLevel > 4 and math.random(100) > 66 then -- defenseLevel = 3-5 : add a Shilka table.insert(groupDefinition.units, { "ZSU-23-4 Shilka", cell = 3, random = true }) elseif defenseLevel > 3 and math.random(100) > 66 then -- defenseLevel = 1 : add a ZU23 on a truck table.insert(groupDefinition.units, { "Ural-375 ZU-23", cell = 3, random = true }) end groupDefinition = veafUnits.processGroup(groupDefinition) veafSpawn.doSpawnGroup(groupPosition, 0, groupDefinition, nil, "RUSSIA", 0, math.random(359), math.random(3,6), groupName, true, false, true, true) end --- Generates a transport mission function veafTransportMission.generateTransportMission(targetSpot, size, defense, blocade, from) veaf.loggers.get(veafTransportMission.Id):debug("generateTransportMission(size = %s, defense=%s, blocade=%d, from=%s)", veaf.p(size), veaf.p(defense), veaf.p(blocade), veaf.p(from)) veaf.loggers.get(veafTransportMission.Id):debug("generateTransportMission: targetSpot ", veaf.p(targetSpot)) if veafTransportMission.friendlyGroupAliveCheckTaskID ~= 'none' then trigger.action.outText("A transport mission already exists !", 5) return end if not from then trigger.action.outText("The \"from\" keyword is mandatory !", 5) return end local startPoint = veafNamedPoints.getPoint(from) if not(startPoint) then trigger.action.outText("A point named "..from.." cannot be found !", 5) return end local friendlyUnits = {} local routeDistance = 0 -- generate a friendly group around the target target spot local groupPosition = veaf.findPointInZone(targetSpot, 100, false) if groupPosition ~= nil then veaf.loggers.get(veafTransportMission.Id):trace("groupPosition=" .. veaf.vecToString(groupPosition)) groupPosition = { x = groupPosition.x, z = groupPosition.y, y = 0 } groupPosition = veaf.placePointOnLand(groupPosition) veaf.loggers.get(veafTransportMission.Id):trace("groupPosition on land=" .. veaf.vecToString(groupPosition)) -- compute player route to friendly group local vecAB = {x = groupPosition.x +- startPoint.x, y = 0, z = groupPosition.z - startPoint.z} routeDistance = mist.vec.mag(vecAB) veaf.loggers.get(veafTransportMission.Id):trace("routeDistance="..routeDistance) if routeDistance < veafTransportMission.MinimumRouteDistance then trigger.action.outText("This drop zone is too close ; you have to place it at least " .. veafTransportMission.MinimumRouteDistance / 1000 .. " km away from point "..from.." !", 5) return end veafTransportMission.generateFriendlyGroup(groupPosition) else veaf.loggers.get(veafTransportMission.Id):info("cannot find a suitable position for friendly group") return end -- generate cargo to be picked up near the player helo veaf.loggers.get(veafTransportMission.Id):debug("Generating cargo") local startPosition = veaf.placePointOnLand(startPoint) veaf.loggers.get(veafTransportMission.Id):trace("startPosition=" .. veaf.vecToString(startPosition)) for i = 1, size do local spawnSpot = { x = startPosition.x + 50, z = startPosition.z + i * 10, y = startPosition.y } veaf.loggers.get(veafTransportMission.Id):trace("spawnSpot=" .. veaf.vecToString(spawnSpot)) local cargoType = veafTransportMission.CargoTypes[math.random(#veafTransportMission.CargoTypes)] local cargoName = veafTransportMission.BlueCargoName .. " #" .. i veafSpawn.doSpawnCargo(spawnSpot, 0, cargoType, "USA") end veaf.loggers.get(veafTransportMission.Id):debug("Done generating cargo") -- generate enemy air defense on the way if defense > 0 then veaf.loggers.get(veafTransportMission.Id):debug("Generating air defense") -- place groups on the way local startingDistance = routeDistance * veafTransportMission.SafeZoneDistance -- enemy presence start after the safe zone local defendedDistance = routeDistance - veafTransportMission.DropZoneSafeZoneDistance - startingDistance local distanceStep = veafTransportMission.EnemyDefenseDistanceStep local nbSteps = math.floor(defendedDistance / distanceStep) local groupNum = 1 for stepNum = 1, nbSteps do local distanceFromStartingPoint = startingDistance + stepNum * distanceStep + math.random(distanceStep/5, 4*distanceStep/5) veaf.loggers.get(veafTransportMission.Id):trace("distanceFromStartingPoint="..distanceFromStartingPoint) -- place an enemy defense group along the way local offset = math.random(veafTransportMission.LeftOrRightMinOffset, veafTransportMission.LeftOrRightMaxOffset) if math.random(100) < 51 then offset = -offset end veaf.loggers.get(veafTransportMission.Id):trace("offset="..offset) local spawnPoint = veaf.computeCoordinatesOffsetFromRoute(startPoint, groupPosition, distanceFromStartingPoint, offset) local groupName = veafTransportMission.RedDefenseGroupName .. " #" .. groupNum veafTransportMission.generateEnemyDefenseGroup(spawnPoint, groupName, defense) groupNum = groupNum + 1 -- place a random number of defense groups further away local nbFarGroups = math.random(0,1) if defense > 4 then nbFarGroups = math.random(1,3) end for _ = 1, nbFarGroups do local offset = math.random(veafTransportMission.LeftOrRightMinFarOffset, veafTransportMission.LeftOrRightMaxFarOffset) if math.random(100) < 51 then offset = -offset end veaf.loggers.get(veafTransportMission.Id):trace("offset="..offset) local spawnPoint = veaf.computeCoordinatesOffsetFromRoute(startPoint, groupPosition, distanceFromStartingPoint, offset) local groupName = veafTransportMission.RedDefenseGroupName .. " #" .. groupNum veafTransportMission.generateEnemyDefenseGroup(spawnPoint, groupName, defense) groupNum = groupNum + 1 end end veaf.loggers.get(veafTransportMission.Id):debug("Done generating air defense") end -- generate enemy blocade forces if blocade > 0 then veaf.loggers.get(veafTransportMission.Id):debug("Generating blocade") -- TODO veaf.loggers.get(veafTransportMission.Id):debug("Done generating blocade") end -- add radio menu for drop zone information (by player group) veafRadio.addCommandToSubmenu('Drop zone information', veafTransportMission.rootPath, veafTransportMission.reportTargetInformation, nil, veafRadio.USAGE_ForGroup) -- add radio menus for commands veafRadio.addSecuredCommandToSubmenu('Skip current objective', veafTransportMission.rootPath, veafTransportMission.skip) veafTransportMission.targetMarkersPath = veafRadio.addSubMenu("Drop zone markers", veafTransportMission.rootPath) veafRadio.addCommandToSubmenu('Request smoke on drop zone', veafTransportMission.targetMarkersPath, veafTransportMission.smokeTarget) veafRadio.addCommandToSubmenu('Request illumination flare over drop zone', veafTransportMission.targetMarkersPath, veafTransportMission.flareTarget) local message = "See F10 radio menu for details\n" -- TODO trigger.action.outText(message,5) veafRadio.refreshRadioMenu() -- start checking for targets destruction veafTransportMission.friendlyGroupWatchdog() end --- Checks if the friendly group is still alive, and if not announces the failure of the transport mission function veafTransportMission.friendlyGroupWatchdog() local nbVehicles, nbInfantry = veafUnits.countInfantryAndVehicles(veafTransportMission.BlueGroupName) if nbVehicles + nbInfantry > 0 then ----veaf.loggers.get(veafTransportMission.Id):trace("Group is still alive with "..nbVehicles.." vehicles and "..nbInfantry.." soldiers") veafTransportMission.friendlyGroupAliveCheckTaskID = mist.scheduleFunction(veafTransportMission.friendlyGroupWatchdog,{},timer.getTime()+veafTransportMission.SecondsBetweenWatchdogChecks) else trigger.action.outText("Friendly group has been destroyed! The mission is a failure!", 5) veafTransportMission.cleanupAfterMission() end end function veafTransportMission.reportTargetInformation(unitName) -- generate information dispatch local nbVehicles, nbInfantry = veafUnits.countInfantryAndVehicles(veafTransportMission.BlueGroupName) local message = "DROP ZONE : ressuply a group of " .. nbVehicles .. " vehicles and " .. nbInfantry .. " soldiers.\n" message = message .. "\n" if veafTransportMission.DoRadioTransmission then message = message .. "NAVIGATION: They will transmit on 550 kHz every " .. veafTransportMission.SecondsBetweenAdfLoops .. " seconds.\n" end -- add coordinates and position from bullseye local averageGroupPosition = veaf.getAveragePosition(veafTransportMission.BlueGroupName) local lat, lon = coord.LOtoLL(averageGroupPosition) local mgrsString = mist.tostringMGRS(coord.LLtoMGRS(lat, lon), 3) local bullseye = mist.utils.makeVec3(mist.DBs.missionData.bullseye.blue, 0) local vec = {x = averageGroupPosition.x - bullseye.x, y = averageGroupPosition.y - bullseye.y, z = averageGroupPosition.z - bullseye.z} local dir = mist.utils.round(mist.utils.toDegree(mist.utils.getDir(vec, bullseye)), 0) local dist = mist.utils.get2DDist(averageGroupPosition, bullseye) local distMetric = mist.utils.round(dist/1000, 0) local distImperial = mist.utils.round(mist.utils.metersToNM(dist), 0) local fromBullseye = string.format('%03d', dir) .. ' for ' .. distMetric .. 'km /' .. distImperial .. 'nm' message = message .. "LAT LON (decimal): " .. mist.tostringLL(lat, lon, 2) .. ".\n" message = message .. "LAT LON (DMS) : " .. mist.tostringLL(lat, lon, 0, true) .. ".\n" message = message .. "MGRS/UTM : " .. mgrsString .. ".\n" message = message .. "FROM BULLSEYE : " .. fromBullseye .. ".\n" message = message .. "\n" -- get altitude, qfe and wind information local altitude = veaf.getLandHeight(averageGroupPosition) --local qfeHp = mist.utils.getQFE(averageGroupPosition, false) --local qfeinHg = mist.utils.getQFE(averageGroupPosition, true) local windDirection, windStrength = veaf.getWind(veaf.placePointOnLand(averageGroupPosition)) message = message .. 'DROP ZONE ALT : ' .. altitude .. " meters.\n" --message = message .. 'TARGET QFW : ' .. qfeHp .. " hPa / " .. qfeinHg .. " inHg.\n" local windText = 'no wind.\n' if windStrength > 0 then windText = string.format( 'from %s at %s m/s.\n', windDirection, windStrength) end message = message .. 'WIND OVER DROP ZONE : ' .. windText -- send message only for the unit veaf.outTextForUnit(unitName, message, 30) end --- add a smoke marker over the drop zone function veafTransportMission.smokeTarget() veaf.loggers.get(veafTransportMission.Id):debug("smokeTarget()") veafSpawn.spawnSmoke(veaf.getAveragePosition(veafTransportMission.BlueGroupName), trigger.smokeColor.Green) trigger.action.outText('Copy smoke requested, GREEN smoke marks the drop zone!',5) veafRadio.delCommand(veafTransportMission.targetMarkersPath, 'Request smoke on drop zone') veafRadio.addCommandToSubmenu('Drop zone is marked with GREEN smoke', veafTransportMission.targetMarkersPath, veaf.emptyFunction) veafTransportMission.smokeResetTaskID = mist.scheduleFunction(veafTransportMission.smokeReset,{},timer.getTime()+veafTransportMission.SecondsBetweenSmokeRequests) veafRadio.refreshRadioMenu() end --- Reset the smoke request radio menu function veafTransportMission.smokeReset() veaf.loggers.get(veafTransportMission.Id):debug("smokeReset()") veafRadio.delCommand(veafTransportMission.targetMarkersPath, 'Drop zone is marked with GREEN smoke') veafRadio.addCommandToSubmenu('Request smoke on drop zone', veafTransportMission.targetMarkersPath, veafTransportMission.smokeTarget) trigger.action.outText('Smoke marker over drop zone available',5) veafRadio.refreshRadioMenu() end --- add an illumination flare over the target area function veafTransportMission.flareTarget() veaf.loggers.get(veafTransportMission.Id):debug("flareTarget()") veafSpawn.spawnIlluminationFlare(veaf.getAveragePosition(veafTransportMission.BlueGroupName)) trigger.action.outText('Copy illumination flare requested, illumination flare over target area!',5) veafRadio.delCommand(veafTransportMission.targetMarkersPath, 'Request illumination flare over drop zone') veafRadio.addCommandToSubmenu('Drop zone is lit with illumination flare', veafTransportMission.targetMarkersPath, veaf.emptyFunction) veafTransportMission.flareResetTaskID = mist.scheduleFunction(veafTransportMission.flareReset,{},timer.getTime()+veafTransportMission.SecondsBetweenFlareRequests) veafRadio.refreshRadioMenu() end --- Reset the flare request radio menu function veafTransportMission.flareReset() veaf.loggers.get(veafTransportMission.Id):debug("flareReset()") veafRadio.delCommand(veafTransportMission.targetMarkersPath, 'Drop zone is lit with illumination flare') veafRadio.addCommandToSubmenu('Request illumination flare over drop zone', veafTransportMission.targetMarkersPath, veafTransportMission.flareTarget) trigger.action.outText('Illumination flare over drop zone available',5) veafRadio.refreshRadioMenu() end --- Called from the "Skip delivery" radio menu : remove the current transport mission function veafTransportMission.skip() veafTransportMission.cleanupAfterMission() trigger.action.outText("Transport mission cleaned up.", 5) end --- Cleanup after either mission is ended or aborted function veafTransportMission.cleanupAfterMission() veaf.loggers.get(veafTransportMission.Id):trace("cleanupAfterMission()") -- destroy groups veaf.loggers.get(veafTransportMission.Id):trace("destroy friendly group") local group = Group.getByName(veafTransportMission.BlueGroupName) if group and group:isExist() == true then group:destroy() end veaf.loggers.get(veafTransportMission.Id):trace("destroy cargos") local unitNum = 1 local doIt = true while doIt do local cargo = StaticObject.getByName(veafTransportMission.BlueCargoName.." #"..unitNum) if cargo and cargo:isExist() == true then cargo:destroy() unitNum = unitNum + 1 else doIt = false end end veaf.loggers.get(veafTransportMission.Id):trace("destroy enemy defense group") local groupNum = 1 local doIt = true while doIt do group = Group.getByName(veafTransportMission.RedDefenseGroupName.." #"..groupNum) if group and group:isExist() == true then group:destroy() groupNum = groupNum + 1 else doIt = false end end veaf.loggers.get(veafTransportMission.Id):trace("destroy enemy blocade group") group = Group.getByName(veafTransportMission.RedBlocadeGroupName) if group and group:isExist() == true then group:destroy() end -- remove the watchdog function veaf.loggers.get(veafTransportMission.Id):trace("remove the watchdog function") if veafTransportMission.friendlyGroupAliveCheckTaskID ~= 'none' then mist.removeFunction(veafTransportMission.friendlyGroupAliveCheckTaskID) end veafTransportMission.friendlyGroupAliveCheckTaskID = 'none' -- remove the watchdog function veaf.loggers.get(veafTransportMission.Id):trace("remove the adf loop function") if veafTransportMission.friendlyGroupAdfLoopTaskID ~= 'none' then mist.removeFunction(veafTransportMission.friendlyGroupAdfLoopTaskID) end veafTransportMission.friendlyGroupAdfLoopTaskID = 'none' veafRadio.delCommand(veafTransportMission.rootPath, 'Skip current objective') veafRadio.delCommand(veafTransportMission.rootPath, 'Get current objective situation') veafRadio.delCommand(veafTransportMission.rootPath, 'Drop zone markers') veafRadio.delSubmenu(veafTransportMission.targetMarkersPath, veafTransportMission.rootPath) veafRadio.refreshRadioMenu() veaf.loggers.get(veafTransportMission.Id):trace("cleanupAfterMission DONE") end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Build the initial radio menu function veafTransportMission.buildRadioMenu() veafTransportMission.rootPath = veafRadio.addSubMenu(veafTransportMission.RadioMenuName) veafRadio.addCommandToSubmenu("HELP", veafTransportMission.rootPath, veafTransportMission.help, nil, veafRadio.USAGE_ForGroup) -- TODO add this command when the respawn will work (see veafTransportMission.resetAllCargoes) -- missionCommands.addCommand('Respawn all cargoes', veafTransportMission.rootPath, veafTransportMission.resetAllCargoes) end function veafTransportMission.help(unitName) local text = 'Create a marker and type "_transport" in the text\n' .. 'This will create a default friendly group awaiting cargo that you need to transport\n' .. 'You can add options (comma separated) :\n' .. ' "defense [0-5]" to specify air defense cover on the way (1 = light, 5 = heavy)\n' .. ' defense = 1 : 3-7 soldiers, GAZ-3308 transport\n' .. ' defense = 2 : 3-7 soldiers, BTR-80 APC\n' .. ' defense = 3 : 3-7 soldiers, chance of BMP-1 IFV, chance of Igla manpad\n' .. ' defense = 4 : 3-7 soldiers, big chance of BMP-1 IFV, big chance of Igla-S manpad, chance of ZU-23 on a truck\n' .. ' defense = 5 : 3-7 soldiers, BMP-1 IFV, big chance of Igla-S manpad, chance of ZSU-23-4 Shilka\n' .. ' "size [1-5]" to change the number of cargo items to be transported (1 per participating helo, usually)\n' .. ' "blocade [0-5]" to specify enemy blocade around the drop zone (1 = light, 5 = heavy)' veaf.outTextForUnit(unitName, text, 30) end function veafTransportMission.endTransportOfCargo(cargoName) local text = 'Congratulations on a job well done ! Cargo ' .. cargoName .. ' has been delivered safely' trigger.action.outText(text, 15) -- TODO reset cargo position -- mist.respawnGroup(cargoName, 15) -- does not work yet because 1. the unit name is changed by mist and 2. the trigger zone condition does not work with the new unit (maybe bc of 1. ?) end function veafTransportMission.resetAllCargoes() -- does not work yet (see veafTransportMission.endTransportOfCargo) local lunits = mist.DBs.unitsByNum if lunits then for i = 1, #lunits do if lunits[i] and lunits[i].unitName and lunits[i].unitName:lower():find('cargo - ') then local name = lunits[i].unitName -- destroy cargo static unit local c = StaticObject.getByName(name) if c then StaticObject.destroy(c) end mist.respawnGroup(name, true) end end end trigger.action.outText("All cargoes have been respawned", 15) end function veafTransportMission.initializeAllHelosInCTLD() veaf.loggers.get(veafTransportMission.Id):warn("Please use ctld.autoInitializeAllHumanTransports - it's automatically run by the veaf.lua script") end function veafTransportMission.initializeAllLogisticInCTLD() veaf.loggers.get(veafTransportMission.Id):warn("Please use ctld.autoInitializeAllLogistic - it's automatically run by the veaf.lua script") end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafTransportMission.initialize() veafTransportMission.buildRadioMenu() veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafTransportMission.onEventMarkChange) end veaf.loggers.get(veafTransportMission.Id):info(string.format("Loading version %s", veafTransportMission.Version)) --- Enable/Disable error boxes displayed on screen. env.setErrorMessageBoxEnabled(false) ------------------ END script veafTransportMission.lua ------------------ ------------------ START script veafUnits.lua ------------------ ------------------------------------------------------------------ -- VEAF groups and units database for DCS World -- By zip (2018) -- -- Features: -- --------- -- * Contains all the units aliases and groups definitions used by the other VEAF scripts -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafUnits = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the root VEAF constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafUnits.Id = "UNITS" --- Version. veafUnits.Version = "1.15.0" -- trace level, specific to this module --veafUnits.LogLevel = "trace" veaf.loggers.new(veafUnits.Id, veafUnits.LogLevel) --- If no unit is spawned in a cell, it will default to this width veafUnits.DefaultCellWidth = 10 --- If no unit is spawned in a cell, it will default to this height veafUnits.DefaultCellHeight = 10 --- Group format that will be spawned then destroyed from a convoy to fix the AI's dumb pathfinding as of 17/08/2022 veafUnits.DefaultPathfindingUnitType = "TZ-22_KrAZ" veafUnits.DefaultPathfindingGroup = {} veafUnits.DefaultPathfindingGroup = { disposition = {h=1, w=1}, units = { {veafUnits.DefaultPathfindingUnitType, random = true} }, groupName = "Pathfinder", description = "Plz Fix ED" } --- delay before the pathfinding fix unit is destroyed veafUnits.delayBeforePathfindingFix = 5 --- if true, the groups and units lists will be printed to the logs, so they can be saved to the documentation files veafUnits.OutputListsForDocumentation = false ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafUnits.traceGroup(group, cells) if group and veafUnits.Trace then veaf.loggers.get(veafUnits.Id):trace("") veaf.loggers.get(veafUnits.Id):trace(" Group : " .. group.description) veaf.loggers.get(veafUnits.Id):trace("") local nCols = group.disposition.w local nRows = group.disposition.h local line1 = "| |" local line2 = "|----|" for nCol = 1, nCols do line1 = line1 .. " ".. string.format("%02d", nCol) .." |" line2 = line2 .. "--------------------------------|" end veaf.loggers.get(veafUnits.Id):trace(line1) veaf.loggers.get(veafUnits.Id):trace(line2) local unitCounter = 1 for nRow = 1, nRows do local line1 = "| |" local line2 = "| " .. string.format("%02d", nRow) .. " |" local line3 = "| |" local line4 = "|----|" for nCol = 1, nCols do local cellNum = (nRow - 1) * nCols + nCol local cell = cells[cellNum] local left = " " local top = " " local right = " " local bottom = " " local bottomleft = " " local center = " " if cell then local unit = cell.unit if unit then local unitName = unit.typeName if unitName:len() > 11 then unitName = unitName:sub(1,11) end unitName = string.format("%02d", unitCounter) .. "-" .. unitName local spaces = 14 - unitName:len() for i=1, math.floor(spaces/2) do unitName = " " .. unitName end for i=1, math.ceil(spaces/2) do unitName = unitName .. " " end center = " " .. unitName .. " " bottomleft = string.format(" %03d ", mist.utils.toDegree(unit.spawnPoint.hdg)) unitCounter = unitCounter + 1 end left = string.format("%08d",math.floor(cell.left)) top = string.format("%08d",math.floor(cell.top)) right = string.format("%08d",math.floor(cell.right)) bottom = string.format("%08d",math.floor(cell.bottom)) end line1 = line1 .. " " .. top .. " " .. "|" line2 = line2 .. "" .. left .. center .. right.. "|" line3 = line3 .. bottomleft .. bottom.. " |" line4 = line4 .. "--------------------------------|" end veaf.loggers.get(veafUnits.Id):trace(line1) veaf.loggers.get(veafUnits.Id):trace(line2) veaf.loggers.get(veafUnits.Id):trace(line3) veaf.loggers.get(veafUnits.Id):trace(line4) end end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Core methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Browse all the units in a group and counts the infantry and vehicles remaining function veafUnits.countInfantryAndVehicles(groupname) local nbVehicles = 0 local nbInfantry = 0 local group = Group.getByName(groupname) if group and group:isExist() == true and #group:getUnits() > 0 then for _, u in pairs(group:getUnits()) do local typeName = u:getTypeName() if typeName then local unit = veafUnits.findUnit(typeName) if unit then if unit.vehicle then nbVehicles = nbVehicles + 1 elseif unit.infantry then nbInfantry = nbInfantry + 1 end end end end end return nbVehicles, nbInfantry end --- searches the DCS database for a unit having this type (case insensitive) function veafUnits.findDcsUnit(unitType) veaf.loggers.get(veafUnits.Id):trace("veafUnits.findDcsUnit(unitType=" .. unitType .. ")") -- find the desired unit in the DCS units database local unit = nil for _, u in pairs(dcsUnits.DcsUnitsDatabase) do if (u and u.type and unitType:lower() == u.type:lower()) or (u and u.name and unitType:lower() == u.name:lower()) then unit = u break end end return unit end --- process a group definition and return a usable group table function veafUnits.processGroup(group) local result = {} -- initialize result table and copy metadata result.disposition = {} result.disposition.h = group.disposition.h result.disposition.w = group.disposition.w result.description = group.description result.groupName = group.groupName result.units = {} veaf.loggers.get(veafUnits.Id):trace("group="..veaf.p(group)) local unitNumber = 1 -- replace all units with a simplified structure made from the DCS unit metadata structure for i = 1, #group.units do local unitType local cell = nil local number = nil local size = nil local hdg = nil local random = false local fitToUnit = false local u = group.units[i] veaf.loggers.get(veafUnits.Id):trace("u="..veaf.p(u)) if type(u) == "string" then -- information was skipped using simplified syntax unitType = u else unitType = u.typeName if not unitType then unitType = u[1] end veaf.loggers.get(veafUnits.Id):trace("unitType="..veaf.p(unitType)) cell = u.cell number = u.number size = u.size hdg = u.hdg if type(size) == "number" then size = {} size.width = u.size size.height = u.size end if u.random then random = true end if u.fitToUnit then fitToUnit = true end end if not(number) then number = 1 end if type(number) == "table" then -- create a random number of units local min = number.min local max = number.max if not(min) then min = 1 end if not(max) then max = 1 end number = math.random(min, max) end if not(hdg) then hdg = math.random(0, 359) -- default heading is random end veaf.loggers.get(veafUnits.Id):trace(string.format("hdg=%d",hdg)) for numUnit = 1, number do veaf.loggers.get(veafUnits.Id):trace("searching for unit [" .. unitType .. "] listed in group [" .. group.groupName .. "]") local unit = veafUnits.findUnit(unitType) if not(unit) then veaf.loggers.get(veafUnits.Id):info("cannot find unit [" .. unitType .. "] listed in group [" .. group.groupName .. "]") else unit.cell = cell unit.hdg = hdg unit.random = random unit.fitToUnit = fitToUnit unit.size = size result.units[unitNumber] = unit unitNumber = unitNumber + 1 end end end -- check group type (WARNING : unit types should not be mixed !) for _, unit in pairs(result.units) do if unit.naval then result.naval = true break end if unit.air then result.air = true break end end veaf.loggers.get(veafUnits.Id):trace("result="..veaf.p(result)) return result end --- searches the database for a group having this alias (case insensitive) function veafUnits.findGroup(groupAlias) veaf.loggers.get(veafUnits.Id):debug("veafUnits.findGroup(groupAlias=" .. groupAlias .. ")") -- find the desired group in the groups database local result = nil for _, g in pairs(veafUnits.GroupsDatabase) do for _, alias in pairs(g.aliases) do if alias:lower() == groupAlias:lower() then result = veafUnits.processGroup(g.group) break end end end return result end --- searches the database for a unit having this alias (case insensitive) function veafUnits.findUnit(unitAlias) veaf.loggers.get(veafUnits.Id):trace("veafUnits.findUnit(unitAlias=" .. unitAlias .. ")") -- find the desired unit in the units database local unit = nil for _, u in pairs(veafUnits.UnitsDatabase) do for _, alias in pairs(u.aliases) do if alias:lower() == unitAlias:lower() then unit = u break end end end if unit then unit = veafUnits.findDcsUnit(unit.unitType) else unit = veafUnits.findDcsUnit(unitAlias) end if not(unit) then veaf.loggers.get(veafUnits.Id):info("cannot find unit [" .. unitAlias .. "]") else unit = veafUnits.makeUnitFromDcsStructure(unit, 1) end return unit end --- Creates a simple structure from DCS complex metadata structure function veafUnits.makeUnitFromDcsStructure(dcsUnit, cell) local result = {} if not(dcsUnit) then return nil end --[[ [9] = { ["type"] = "Vulcan", ["name"] = "AAA Vulcan M163", ["category"] = "Air Defence", ["vehicle"] = true, ["description"] = "AAA Vulcan M163", ["aliases"] = { [1] = "M163 Vulcan", }, -- end of ["aliases"] }, -- end of [9] ]] result.category = dcsUnit.category result.typeName = dcsUnit.type result.displayName = dcsUnit.description result.naval = (dcsUnit.naval) result.air = (dcsUnit.air) if (not(dcsUnit.naval) and not(dcsUnit.air) and not(dcsUnit.infantry) and not(dcsUnit.vehicle) and (dcsUnit.attribute==nil or dcsUnit.attribute.Fortifications==nil)) then result.static = true end result.infantry = (dcsUnit.infantry) result.vehicle = (dcsUnit.vehicle) --[[ result.size = { x = veaf.round(dcsUnit.desc.box.max.x - dcsUnit.desc.box.min.x, 1), y = veaf.round(dcsUnit.desc.box.max.y - dcsUnit.desc.box.min.y, 1), z = veaf.round(dcsUnit.desc.box.max.z - dcsUnit.desc.box.min.z, 1)} result.width = result.size.z result.length= result.size.x -- invert if width > height if result.width > result.length then local width = result.width result.width = result.length result.length = width end ]] result.cell = cell return result end --- checks if position is correct for the unit type function veafUnits.checkPositionForUnit(spawnPosition, unit) veaf.loggers.get(veafUnits.Id):trace("checkPositionForUnit()") veaf.loggers.get(veafUnits.Id):trace("spawnPosition=%s", spawnPosition) local vec2 = { x = spawnPosition.x, y = spawnPosition.z } veaf.loggers.get(veafUnits.Id):trace("vec2=%s", vec2) veaf.loggers.get(veafUnits.Id):trace("unit=%s", unit) local landType = land.getSurfaceType(vec2) local IsNavalStatic = false --offshore static (list in dcsUnits.lua) flag if unit.static and veaf.findInTable(dcsUnits.NavalStatics, unit.typeName) then veaf.loggers.get(veafUnits.Id):trace("Is Naval Static") IsNavalStatic = true end if landType == land.SurfaceType.WATER then veaf.loggers.get(veafUnits.Id):trace("landType = WATER") else veaf.loggers.get(veafUnits.Id):trace("landType = GROUND") end if spawnPosition then if unit.air then -- if the unit is a plane or helicopter if spawnPosition.z <= 10 then -- if lower than 10m don't spawn unit return false end elseif unit.naval or IsNavalStatic then -- if the unit is a naval unit or an offshore static if landType ~= land.SurfaceType.WATER then -- don't spawn over anything but water return false end else if landType == land.SurfaceType.WATER then -- don't spawn over water return false end end end return true end --- Adds a placement point to every unit of the group, centering the whole group around the spawnPoint, and adding an optional spacing function veafUnits.placeGroup(group, spawnPoint, spacing, hdg, hasDest) veaf.loggers.get(veafUnits.Id):trace("group = %s",group) veaf.loggers.get(veafUnits.Id):trace("spawnPoint = %s", spawnPoint) veaf.loggers.get(veafUnits.Id):trace("spacing = %s", spacing) veaf.loggers.get(veafUnits.Id):trace("hdg = %s", hdg) veaf.loggers.get(veafUnits.Id):trace("hasDest = %s", hasDest) if not(hdg) then hdg = 0 -- default north end local hasDest = false or hasDest veaf.loggers.get(veafUnits.Id):trace(string.format("hasDest = %s", veaf.p(hasDest))) if not(group.disposition) then -- default disposition is a square local l = math.ceil(math.sqrt(#group.units)) group.disposition = { h = l, w = l} end local nRows = nil local nCols = nil if hasDest then local pathfindingFixer = veafUnits.processGroup(veafUnits.DefaultPathfindingGroup) --insert a unit (structured into a group) that will be destroyed just after the convoy is spawned, this is to fix the AI weird pathfinding table.insert(group.units, pathfindingFixer.units[1]) --insert the unit that has all of the necessary info into the group that's being placed nRows = #group.units nCols = 1 else nRows = group.disposition.h nCols = group.disposition.w end -- sort the units by occupied cell local fixedUnits = {} local freeUnits = {} for _, unit in pairs(group.units) do if unit.cell and not hasDest then --if the group has a destination, programmer defined patterns do not apply anymore as the convoy is spawned in a line table.insert(fixedUnits, unit) else table.insert(freeUnits, unit) end end local cells = {} local allCells = {} for cellNum = 1, nRows*nCols do allCells[cellNum] = cellNum end -- place fixed units in their designated cells for i = 1, #fixedUnits do local unit = fixedUnits[i] cells[unit.cell] = {} cells[unit.cell].unit = unit -- remove this cell from the list of available cells for cellNum = 1, #allCells do if allCells[cellNum] == unit.cell then table.remove(allCells, cellNum) break end end end -- randomly place non-fixed units in the remaining cells for i = 1, #freeUnits do local randomCellNum = allCells[math.random(1, #allCells)] local unit = freeUnits[i] unit.cell = randomCellNum cells[unit.cell] = {} cells[randomCellNum].unit = unit -- remove this cell from the list of available cells for cellNum = 1, #allCells do if allCells[cellNum] == unit.cell then table.remove(allCells, cellNum) break end end end if hasDest then local cellGreater = function(unit1, unit2) if unit1 and unit2 and unit1.cell < unit2.cell then return true else return false end end table.sort(group.units, cellGreater) end -- compute the size of the cells, rows and columns local cols = {} local rows = {} for nRow = 1, nRows do for nCol = 1, nCols do local cellNum = (nRow - 1) * nCols + nCol local cell = cells[cellNum] local colWidth = 0 local rowHeight = 0 if cols[nCol] then colWidth = cols[nCol].width end if rows[nRow] then rowHeight = rows[nRow].height end if cell then cell.width = veafUnits.DefaultCellWidth + (spacing * veafUnits.DefaultCellWidth) cell.height = veafUnits.DefaultCellHeight + (spacing * veafUnits.DefaultCellHeight) local unit = cell.unit if unit then unit.cell = cellNum if unit.width and unit.width > 0 then cell.width = unit.width + (spacing * unit.width) end if unit.length and unit.length > 0 then cell.height = unit.length + (spacing * unit.length) end if unit.size then cell.width = unit.size.width + (spacing * unit.size.width) cell.height = unit.size.height + (spacing * unit.size.height) end end if not unit.fitToUnit then -- make the cell square if cell.width > cell.height then cell.height = cell.width elseif cell.width < cell.height then cell.width = cell.height end end if cell.width > colWidth then colWidth = cell.width end if cell.height > rowHeight then rowHeight = cell.height end end cols[nCol] = {} cols[nCol].width = colWidth rows[nRow] = {} rows[nRow].height = rowHeight end end -- compute the size of the grid local totalWidth = 0 local totalHeight = 0 for nCol = 1, #cols do totalWidth = totalWidth + cols[nCol].width end for nRow = 1, #rows do -- bottom -> up totalHeight = totalHeight + rows[#rows-nRow+1].height end veaf.loggers.get(veafUnits.Id):trace(string.format("totalWidth = %d",totalWidth)) veaf.loggers.get(veafUnits.Id):trace(string.format("totalHeight = %d",totalHeight)) -- place the grid local currentColLeft = spawnPoint.z - totalWidth/2 local currentColTop = spawnPoint.x - totalHeight/2 for nCol = 1, #cols do veaf.loggers.get(veafUnits.Id):trace(string.format("currentColLeft = %d",currentColLeft)) cols[nCol].left = currentColLeft cols[nCol].right= currentColLeft + cols[nCol].width currentColLeft = cols[nCol].right end for nRow = 1, #rows do -- bottom -> up veaf.loggers.get(veafUnits.Id):trace(string.format("currentColTop = %d",currentColTop)) rows[#rows-nRow+1].bottom = currentColTop rows[#rows-nRow+1].top = currentColTop + rows[#rows-nRow+1].height currentColTop = rows[#rows-nRow+1].top end -- compute the centers and extents of the cells for nRow = 1, nRows do for nCol = 1, nCols do local cellNum = (nRow - 1) * nCols + nCol local cell = cells[cellNum] if cell then cell.top = rows[nRow].top cell.bottom = rows[nRow].bottom cell.left = cols[nCol].left cell.right = cols[nCol].right cell.center = {} cell.center.x = cell.left + math.random((cell.right - cell.left) / 10, (cell.right - cell.left) - ((cell.right - cell.left) / 10)) cell.center.y = cell.top + math.random((cell.bottom - cell.top) / 10, (cell.bottom - cell.top) - ((cell.bottom - cell.top) / 10)) end end end --find the heading offset relative to the group's heading to spawn the units perpendicular to the road -- local convoyHDGoffset = 90 -- if hasDest then -- local road_x, road_z = land.getClosestPointOnRoads('roads',spawnPoint.x, spawnPoint.z) -- local roadPoint = veaf.placePointOnLand({x = road_x, y = 0, z = road_z}) -- local nearestRoadHDG = mist.utils.getHeadingPoints(spawnPoint, roadPoint,false) * 180 / math.pi -- veaf.loggers.get(veafUnits.Id):trace(string.format("HDG to nearest road : %s", veaf.p(nearestRoadHDG))) -- veaf.loggers.get(veafUnits.Id):trace(string.format("Group HDG : %s", veaf.p(hdg))) -- if nearestRoadHDG then -- nearestRoadHDG = nearestRoadHDG - hdg -- if nearestRoadHDG < 0 then -- nearestRoadHDG = nearestRoadHDG + 360 -- end -- if nearestRoadHDG >= 180 then -- convoyHDGoffset = 270 -- end -- end -- end -- randomly place the units for _, cell in pairs(cells) do veaf.loggers.get(veafUnits.Id):trace(string.format("cell = %s",veaf.p(cell))) local unit = cell.unit if unit then unit.spawnPoint = {} if not cell.center then veaf.loggers.get(veafUnits.Id):error(string.format("Cannot find cell.center !")) veaf.loggers.get(veafUnits.Id):error(string.format("cell = %s",veaf.p(cell))) veaf.loggers.get(veafUnits.Id):error(string.format("group = %s",veaf.p(group))) end unit.spawnPoint.z = cell.center.x if unit.random and spacing > 0 then unit.spawnPoint.z = unit.spawnPoint.z + math.random(-((spacing-1) * (unit.width or veafUnits.DefaultCellWidth))/2, ((spacing-1) * (unit.width or veafUnits.DefaultCellWidth))/2) end unit.spawnPoint.x = cell.center.y if unit.random and spacing > 0 then unit.spawnPoint.x = unit.spawnPoint.x + math.random(-((spacing-1) * (unit.length or veafUnits.DefaultCellHeight))/2, ((spacing-1) * (unit.length or veafUnits.DefaultCellHeight))/2) end unit.spawnPoint.y = spawnPoint.y -- take into account group rotation, if needed if hdg > 0 then local angle = mist.utils.toRadian(hdg) local x = unit.spawnPoint.z - spawnPoint.z local y = unit.spawnPoint.x - spawnPoint.x local x_rotated = x * math.cos(angle) + y * math.sin(angle) local y_rotated = -x * math.sin(angle) + y * math.cos(angle) unit.spawnPoint.z = x_rotated + spawnPoint.z unit.spawnPoint.x = y_rotated + spawnPoint.x end -- unit heading if hasDest then --apply the offset when the group has a destination, 0 will make them spawn in line, 90 or 270 perpendicular to the group's hdg (the road if the group's hdg was set properly) etc. unit.hdg = 0 --convoyHDGoffset end if unit.hdg then local unitHeading = unit.hdg + hdg -- don't forget to add group heading if unitHeading > 360 then unitHeading = unitHeading - 360 end unit.spawnPoint.hdg = mist.utils.toRadian(unitHeading) else unit.spawnPoint.hdg = 0 -- due north end end end return group, cells end function veafUnits.removePathfindingFixUnit(groupName) local group = Group.getByName(groupName) if group then local units = group:getUnits() if units then for _,unit in pairs(units) do if unit then local unitType = unit:getTypeName() if unitType and unitType == veafUnits.DefaultPathfindingUnitType then unit:destroy() break end end end end end end function veafUnits.logGroupsListInMarkdown() local function _sortGroupNameCaseInsensitive(g1,g2) if g1 and g1.group and g1.group.groupName and g2 and g2.group and g2.group.groupName then return string.lower(g1.group.groupName) < string.lower(g2.group.groupName) else return string.lower(g1) < string.lower(g2) end end local text = [[ This goes in [documentation\content\Mission maker\references\group-list.md]: |Name|Description|Aliases| |--|--|--| ]] veaf.loggers.get(veafUnits.Id):info(text) -- make a copy of the table local groupsCopy = {} for _, g in pairs(veafUnits.GroupsDatabase) do if not g.hidden then table.insert(groupsCopy, g) end end -- sort the copy table.sort(groupsCopy, _sortGroupNameCaseInsensitive) -- use the keys to retrieve the values in the sorted order for _, g in pairs(groupsCopy) do text = "|" .. g.group.groupName .. "|" .. g.group.description .. "|" .. table.concat(g.aliases, ", ") .. "|\n" veaf.loggers.get(veafUnits.Id):info(text) end end function veafUnits.logUnitsListInMarkdown() local function _sortUnitNameCaseInsensitive(u1,u2) if u1 and u1.name and u2 and u2.name then return string.lower(u1.name) < string.lower(u2.name) else return string.lower(u1) < string.lower(u2) end end local text = [[ This goes in [documentation\content\Mission maker\references\units-list.md]: |Name|Description|Aliases| |--|--|--| ]] veaf.loggers.get(veafUnits.Id):info(text) -- make a copy of the table local units = {} for k, data in pairs(dcsUnits.DcsUnitsDatabase) do local u = { name = k } for _, aliasData in pairs(veafUnits.UnitsDatabase) do if aliasData and aliasData.unitType and string.lower(aliasData.unitType) == string.lower(k) then u.aliases = aliasData.aliases end end if data then u.description = data.description u.typeName = data.type end table.insert(units, u) end -- sort the copy table.sort(units, _sortUnitNameCaseInsensitive) -- use the keys to retrieve the values in the sorted order for _, u in pairs(units) do -- serialize its fields text = "|" .. u.name .. "|" if u.description then text = text .. u.description end text = text .. "|" if u.aliases then text = text .. table.concat(u.aliases, ", ") end text = text .. "|" veaf.loggers.get(veafUnits.Id):info(text) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Units databases ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafUnits.UnitsDatabase = { { aliases = {"hq7"}, unitType = "HQ-7_LN_SP", }, { aliases = {"hq7eo"}, unitType = "HQ-7_LN_EO", }, { aliases = {"sa8", "sa-8"}, unitType = "Osa 9A33 ln", }, { aliases = {"sa9", "sa-9"}, unitType = "Strela-1 9P31" }, { aliases = {"sa13", "sa-13"}, unitType = "Strela-10M3", }, { aliases = {"sa15", "sa-15"}, unitType = "Tor 9A331", }, { aliases = {"sa18", "sa-18", "manpad"}, unitType = "SA-18 Igla-S manpad", }, { aliases = {"dogear"}, unitType = "Dog Ear radar", }, { aliases = {"shilka"}, unitType = "ZSU-23-4 Shilka", }, { aliases = {"tarawa"}, unitType = "LHA_Tarawa", }, { aliases = {"blue-ewr"}, unitType = "FPS-117", }, { aliases = {"red-ewr"}, unitType = "1L13 EWR", }, { aliases = {"avenger"}, unitType = "M1097 Avenger", }, } ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Groups databases ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Syntax : ------------ -- -- aliases : list of aliases which can be used to designate this group, case insensitive -- disposition : height and width (in cells) of the group layout template (see explanation of group layouts below) -- units : list of all the units composing the group. Each unit in the list is composed of : -- alias : alias of the unit in the VEAF units database, or actual DCS type name in the DCS units database -- cell : preferred layout cell ; the unit will be spawned in this cell, in the layout defined in the *layout* field. (see explanation of group layouts below) ; when nothing else is specified, a number after the unit alias is considered to be the *cell* parameter -- size : fixes the cell size (in meters), instead of relying on the contained unit size (modified with the *spacing* parameter) ; can be either a table with width and height, or a number for square cells -- number : either a number, which will be the quantity of this unit type spawned ; or a table, with *min* and *max* values that will be used to spawn a random quantity of this unit typ -- hdg : the unit heading will mean that, if the group is spawned facing north, this unit will be facing this heading (in degrees). If not set, units will face the group heading -- random : if set, the unit will be placed randomly in the cell, leaving a one unit size margin around. -- fitToUnit : if set, the cell around the unit will not be a square but a rectangle of the unit's exact size (plus the spacing, if set) -- description = human-friendly name for the group -- groupName = name used when spawning this group (will be flavored with a numerical suffix) -- -- empty cells measure 10m x 10m veafUnits.GroupsDatabase = { --China { aliases = {"hq7"}, group = { disposition = { h= 5, w= 5}, units = { --STR {"HQ-7_STR_SP", cell = 8}, --LN {"HQ-7_LN_SP", cell = 1, hdg = 300}, {"HQ-7_LN_SP", cell = 5, hdg = 60}, {"HQ-7_LN_SP", cell = 23, hdg = 180}, --supply truck {"Ural-375", random=true}, }, description = "HQ-7 SAM site", groupName = "HQ-7" }, }, { aliases = {"hq7_single"}, group = { disposition = { h= 3, w= 3}, units = { --LN {"HQ-7_LN_SP", cell = 1}, --supply truck {"Ural-375", random=true}, }, description = "HQ-7 SAM site", groupName = "HQ-7" }, }, { aliases = {"hq7-noew"}, group = { disposition = { h= 5, w= 5}, units = {{"HQ-7_LN_SP", cell = 1, hdg = 300}, {"HQ-7_LN_SP", cell = 5, hdg = 60}, {"HQ-7_LN_SP", cell = 23, hdg = 180}, {"Ural-375", random=true},}, description = "HQ-7 SAM site without STR", groupName = "HQ-7" }, }, { aliases = {"hq7eo"}, group = { disposition = { h= 5, w= 5}, units = { --STR {"HQ-7_STR_SP", cell = 8}, --LN {"HQ-7_LN_EO", cell = 1, hdg = 300}, {"HQ-7_LN_EO", cell = 5, hdg = 60}, {"HQ-7_LN_EO", cell = 23, hdg = 180}, --supply truck {"Ural-375", random=true}, }, description = "HQ-7EO SAM site", groupName = "HQ-7" }, }, { aliases = {"hq7eo_single"}, group = { disposition = { h= 3, w= 3}, units = { --LN {"HQ-7_LN_EO", cell = 1}, --supply truck {"Ural-375", random=true}, }, description = "HQ-7EO SAM site", groupName = "HQ-7" }, }, { aliases = {"hq7eo-noew"}, group = { disposition = { h= 5, w= 5}, units = {{"HQ-7_LN_EO", cell = 1, hdg = 300}, {"HQ-7_LN_EO", cell = 5, hdg = 60}, {"HQ-7_LN_EO", cell = 23, hdg = 180}, {"Ural-375", random=true},}, description = "HQ-7EO SAM site without STR", groupName = "HQ-7" }, }, --Warsaw Pact { aliases = {"sa2", "sa-2", "fs"}, group = { disposition = { h= 6, w= 8}, units = { {"SNR_75V", cell = 20}, {"p-19 s-125 sr", cell = 48}, {"S_75M_Volhov", cell = 2, hdg = 315}, {"S_75M_Volhov", cell = 6, hdg = 45}, {"S_75M_Volhov", cell = 17, hdg = 270}, {"S_75M_Volhov", cell = 24, hdg = 90}, {"S_75M_Volhov", cell = 34, hdg = 225}, {"S_75M_Volhov", cell = 38, hdg = 135}, {"ZSU_57_2", number = {min=1, max=2}, random=true}, {"S-60_Type59_Artillery", number = {min=1, max=2}, random=true}, {"ZIL-135", number = {min = 1, max = 2}, random=true}, }, description = "SA-2 SAM site", groupName = "SA2" }, }, { aliases = {"sa3", "sa-3", "lb"}, group = { disposition = { h= 7, w= 9}, units = { {"p-19 s-125 sr", cell = 1}, {"snr s-125 tr", cell = 33}, {"5p73 s-125 ln", cell = 18, hdg = 30}, {"5p73 s-125 ln", cell = 30, hdg = 270}, {"5p73 s-125 ln", cell = 61, hdg = 150}, {"ZSU_57_2", number = {min=1, max=2}, random=true}, {"S-60_Type59_Artillery", number = {min=1, max=2}, random=true}, {"ZIL-135", number = {min = 1, max = 2}, random=true}, }, description = "SA-3 SAM site", groupName = "SA3" }, }, { -- Sa-5 SAM site (2 launcher battery configuration, IRL 5 maximum could be seen deployed on one site). Note: doesn't come out exactly as planned when spawned, but close enough aliases = {"sa5", "sa-5", "S-200", "S200", "s200", "s-200"}, group = { disposition = {h= 155, w= 120}, units = { -- Search radar unit {"RLS_19J6", cell = 17614, size = 10}, -- Track radar unit {"RPC_5N62V", cell = 15118, size = 10}, --generator units {"generator_5i57", hdg = 80, cell = 15358, size = 10}, {"generator_5i57", hdg = 75, cell = 17854, size = 10}, --Track Radar, Search Radar generator -- launchers --battery 1 {"S-200_Launcher", hdg = 2, cell = 676, size = 10}, {"S-200_Launcher", hdg = 4, cell = 702, size = 10}, {"S-200_Launcher", hdg = 358, cell = 2749, size = 10}, {"S-200_Launcher", hdg = 358, cell = 3069, size = 10}, {"S-200_Launcher", hdg = 358, cell = 4786, size = 10}, {"S-200_Launcher", hdg = 355, cell = 5115, size = 10}, --battery 2 {"S-200_Launcher", hdg = 347, cell = 7231, size = 10}, {"S-200_Launcher", hdg = 343, cell = 8045, size = 10}, {"S-200_Launcher", hdg = 341, cell = 8935, size = 10}, {"S-200_Launcher", hdg = 355, cell = 9962, size = 10}, {"S-200_Launcher", hdg = 350, cell = 10734, size = 10}, {"S-200_Launcher", hdg = 340, cell = 11781, size = 10}, -- missile loading trucks (emulated, 1 per launcher for simplicity, 2 IRL) --battery 1 {"ZIL-135", hdg = 174, cell = 76, size = 10}, {"ZIL-135", hdg = 181, cell = 102, size = 10}, {"ZIL-135", hdg = 194, cell = 2149, size = 10}, {"ZIL-135", hdg = 179, cell = 2469, size = 10}, {"ZIL-135", hdg = 165, cell = 4186, size = 10}, {"ZIL-135", hdg = 183, cell = 4515, size = 10}, --battery 2 {"ZIL-135", hdg = 157, cell = 6631, size = 10}, {"ZIL-135", hdg = 175, cell = 7445, size = 10}, {"ZIL-135", hdg = 165, cell = 8335, size = 10}, {"ZIL-135", hdg = 175, cell = 9362, size = 10}, {"ZIL-135", hdg = 172, cell = 10134, size = 10}, {"ZIL-135", hdg = 179, cell = 11181, size = 10}, -- C2 units --battery 1 {"ZIL-131 KUNG", hdg = 192, cell = 2730, size = 10}, --battery 2 {"ZIL-131 KUNG", hdg = 170, cell = 9262, size = 10}, --site wide {"ZIL-131 KUNG", cell = 14616, size = 10} }, description = "S200 SAM site", groupName = "S200" }, }, { aliases = {"sa6", "sa-6", "06"}, group = { disposition = { h= 7, w= 7}, units = { {"Kub 1S91 str", cell = 25}, {"Kub 2P25 ln", cell = 4, hdg = 180}, {"Kub 2P25 ln", cell = 22, hdg = 90}, {"Kub 2P25 ln", cell = 28, hdg = 270}, {"Kub 2P25 ln", cell = 46, hdg = 0}, {"ZSU-23-4 Shilka", number = {min=1, max=2}, random=true}, {"ATZ-5", random=true}, {"ZIL-135", number = {min = 1, max = 2}, random=true}, {"Ural-375 PBU", random=true} }, description = "SA-6 SAM site", groupName = "SA6" }, }, { aliases = {"sa8_squad"}, group = { disposition = { h= 4, w= 4}, units = {{"Osa 9A33 ln", random = true}, {"GAZ-66", random=true}}, description = "Sa-8 SAM site", groupName = "SA8" }, }, { aliases = {"sa9_squad"}, group = { disposition = { h= 4, w= 4}, units = {{"Strela-1 9P31", random = true}, {"GAZ-66", random=true}}, description = "Sa-9 SAM site", groupName = "SA9" }, }, { aliases = {"sa10", "s300", "bb"}, group = { disposition = { h= 10, w= 13}, units = { {"S-300PS 40B6MD sr", cell = 130}, {"S-300PS 40B6M tr", cell = 7}, {"S-300PS 5P85C ln", cell = 29}, {"S-300PS 5P85D ln", cell = 37}, {"S-300PS 5P85D ln", cell = 43}, {"S-300PS 5P85C ln", cell = 49}, {"S-300PS 5P85C ln", cell = 57}, {"S-300PS 5P85D ln", cell = 61}, {"S-300PS 5P85D ln", cell = 71}, {"S-300PS 5P85C ln", cell = 73}, {"S-300PS 64H6E sr", cell = 98}, {"S-300PS 54K6 cp", cell = 118}, {"ZSU-23-4 Shilka", number = {min=1, max=2}, random=true}, {"2S6 Tunguska", random=true}, }, description = "S300 SAM site", groupName = "S300" }, }, { aliases = {"sa11", "sa-11", "sd"}, group = { disposition = { h= 9, w= 9}, units = { {"SA-11 Buk SR 9S18M1", cell = 42}, {"SA-11 Buk CC 9S470M1", cell = 39}, {"SA-11 Buk LN 9A310M1", cell = 1}, {"SA-11 Buk LN 9A310M1", cell = 5}, {"SA-11 Buk LN 9A310M1", cell = 9}, {"SA-11 Buk LN 9A310M1", cell = 72}, {"SA-11 Buk LN 9A310M1", cell = 76}, {"SA-11 Buk LN 9A310M1", cell = 81}, {"ZSU-23-4 Shilka", number = {min=1, max=2}, random=true}, {"ATZ-5", random=true}, {"Ural-375", number = {min = 1, max = 2}, random=true}, }, description = "SA-11 SAM site", groupName = "SA11" }, }, { aliases = {"sa13_squad"}, group = { disposition = { h= 4, w= 4}, units = {{"Strela-10M3", random = true}, {"GAZ-66", random=true}}, description = "Sa-13 SAM site", groupName = "SA13" }, }, { aliases = {"sa15_squad"}, group = { disposition = { h= 4, w= 4}, units = {{"Tor 9A331", random = true}, {"GAZ-66", random=true}}, description = "Sa-15 SAM site", groupName = "SA15" }, }, { aliases = {"sa15m2_squad"}, group = { disposition = { h= 4, w= 4}, units = {{"CHAP_TorM2", random = true}, {"GAZ-66", random=true}}, description = "Sa-15M2 SAM site", groupName = "SA15" }, }, { aliases = {"sa22_squad"}, group = { disposition = { h= 4, w= 4}, units = {{"CHAP_PantsirS1", random = true}, {"GAZ-66", random=true}}, description = "SA-22 SAM site", groupName = "SA22" }, }, { -- insurgent sa18 squad aliases = {"insurgent_manpad_squad", "ins_manpad"}, group = { disposition = {h= 3, w= 3}, units = { -- IglaS Command Unit {"MANPADS SA-18 Igla \"Grouse\" C2", random=true}, -- IglaS {"Igla manpad INS", random=true}, -- Troops {"Infantry AK Ins", number = {min=2, max=3}, random=true}, -- Transport {"HL_DSHK", random=true} }, description = "Insurgent Sa-18 Manpad Squad", groupName = "Insurgent Manpad Squad" }, }, { -- sa18 squad aliases = {"sa18_squad"}, group = { disposition = {h= 3, w= 3}, units = { -- IglaS Command Unit {"MANPADS SA-18 Igla \"Grouse\" C2", random=true}, -- IglaS {"SA-18 Igla manpad", number = {min=1, max=2}, random=true}, -- Troops {"Infantry AK", number = {min=2, max=4}, random=true}, --Transport {"UAZ-469", random=true} }, description = "Sa-18 Manpad Squad", groupName = "Red Manpad Squad" }, }, { -- sa18s squad aliases = {"sa18s_squad"}, group = { disposition = {h= 4, w= 4}, units = { -- IglaS Command Unit {"MANPADS SA-18 Igla-S \"Grouse\" C2", random=true}, -- IglaS {"SA-18 Igla-S manpad", number = {min=1, max=2}, random=true}, -- Troops {"Infantry AK ver2", number = {min=2, max=6}, random=true}, --Transport {"Tigr_233036", random=true} }, description = "Sa-18S Manpad Squad", groupName = "Red Modern Manpad Squad" }, }, { aliases = {"sa19_squad"}, group = { disposition = { h= 4, w= 4}, units = {{"2S6 Tunguska", random = true}, {"Ural-375", random=true}}, description = "Sa-19 SAM site", groupName = "SA19" }, }, { -- red ewr position aliases = {"red_ewr", "ewr"}, group = { disposition = {h= 7, w= 7}, units = { -- Radar unit {"55G6 EWR", cell = 25}, -- IglaS Command Unit {"MANPADS SA-18 Igla \"Grouse\" C2", random=true}, -- IglaS {"SA-18 Igla manpad", random=true}, -- C2 {"Ural-375 PBU", random=true} }, description = "Red EWR", groupName = "Red EWR" }, }, --NATO { aliases = {"rapier_optical", "rpo"}, group = { disposition = { h= 5, w= 5}, units = { {"rapier_fsa_optical_tracker_unit", cell = 9}, {"rapier_fsa_optical_tracker_unit", cell = 17}, {"rapier_fsa_launcher", cell = 1, hdg = 315}, {"rapier_fsa_launcher", cell = 5, hdg = 45}, {"rapier_fsa_launcher", cell = 21, hdg = 225}, {"rapier_fsa_launcher", cell = 25, hdg = 135}, --supply/crew truck {"Land_Rover_101_FC", number = {min=1, max = 2}, random = true}, }, description = "Rapier SAM site", groupName = "Rapier" }, }, { aliases = {"rapier_radar", "rpr"}, group = { disposition = { h= 5, w= 5}, units = { {"rapier_fsa_blindfire_radar", cell = 13}, {"rapier_fsa_optical_tracker_unit", cell = 9}, {"rapier_fsa_optical_tracker_unit", cell = 17}, {"rapier_fsa_launcher", cell = 1, hdg = 315}, {"rapier_fsa_launcher", cell = 5, hdg = 45}, {"rapier_fsa_launcher", cell = 21, hdg = 225}, {"rapier_fsa_launcher", cell = 25, hdg = 135}, --supply/crew truck {"Land_Rover_101_FC", number = {min=1, max = 2}, random = true}, }, description = "Rapier SAM site with radar", groupName = "Rapier-radar" }, }, { -- Stinger Squad aliases = {"stinger_squad"}, group = { disposition = {h= 4, w= 4}, units = { -- Stinger Command Unit {"MANPADS Stinger C2", random=true}, {"MANPADS Stinger C2", random=true}, -- Stinger {"Soldier stinger", random=true}, {"Soldier stinger", random=true}, -- Troops {"Soldier M4 GRG", number = {min=3, max=4}, random=true}, --Transport {"Hummer", random=true}, {"Hummer", random=true} }, description = "Stinger Manpad Squad", groupName = "Blue Manpad Squad" }, }, { aliases = {"avenger_squad"}, group = { disposition = { h= 4, w= 4}, units = {{"M1097 Avenger", random = true}, {"M 818", random=true}}, description = "Avenger SAM site", groupName = "Avenger" }, }, { aliases = {"roland"}, group = { disposition = { h= 5, w= 5}, units = {{"Roland Radar", cell = 8}, {"Roland ADS", cell = 1 , hdg = 300}, {"Roland ADS", cell = 5, hdg = 60}, {"Roland ADS", cell = 23, hdg = 180}, {"M 818", random=true}}, description = "Roland SAM site", groupName = "Roland" }, }, { aliases = {"roland-noew"}, group = { disposition = { h= 5, w= 5}, units = {{"Roland ADS", cell = 1 , hdg = 300}, {"Roland ADS", cell = 5, hdg = 60}, {"Roland ADS", cell = 23, hdg = 180}, {"M 818", random=true}}, description = "Roland SAM site", groupName = "Roland" }, }, { -- NASAMS SHORAD system with 120C aliases = {"nasams_c", "nasams", "NASAMS", "NASAMS_C"}, group = { disposition = { h= 7, w= 11}, units = { -- Search radar unit {"NASAMS_Radar_MPQ64F1", cell = 41}, -- launchers {"NASAMS_LN_C", hdg = 315, cell = 4}, {"NASAMS_LN_C", hdg = 45, cell = 11}, {"NASAMS_LN_C", hdg = 225, cell = 70}, {"NASAMS_LN_C", hdg = 135, cell = 77}, -- C2 {"NASAMS_Command_Post", cell = 34}, -- a supply truck or three {"M 818", number = {min=1, max=3}, random=true}, --IR defense {"M1097 Avenger", number = {min=1, max=2}, random=true} }, description = "NASAMS C battery", groupName = "NASAMS C battery" }, }, { -- NASAMS SHORAD system with 120B aliases = {"nasams_b", "NASAMS_B"}, group = { disposition = { h= 7, w= 11}, units = { -- Search radar unit {"NASAMS_Radar_MPQ64F1", cell = 41}, -- launchers {"NASAMS_LN_B", hdg = 315, cell = 4}, {"NASAMS_LN_B", hdg = 45, cell = 11}, {"NASAMS_LN_B", hdg = 225, cell = 70}, {"NASAMS_LN_B", hdg = 135, cell = 77}, -- C2 {"NASAMS_Command_Post", cell = 34}, -- a supply truck or three {"M 818", number = {min=1, max=3}, random=true}, --IR defense {"M1097 Avenger", number = {min=1, max=2}, random=true} }, description = "NASAMS B battery", groupName = "NASAMS B battery" }, }, { aliases = {"hawk", "ha", "mim-23"}, group = { disposition = { h= 40, w= 40}, units = { {"Hawk sr", cell = 816}, {"Hawk cwar", cell = 826}, {"Hawk cwar", cell = 1100}, {"Hawk tr", cell = 381, hdg = 0}, {"Hawk tr", cell = 1208, hdg = 240}, {"Hawk tr", cell = 1231, hdg = 120}, {"Hawk pcp", cell = 941}, {"Hawk ln", cell = 22, hdg = 0}, {"Hawk ln", cell = 297, hdg = 300 }, {"Hawk ln", cell = 306, hdg = 60}, {"Hawk ln", cell = 1042, hdg = 300}, {"Hawk ln", cell = 1401, hdg = 240 }, {"Hawk ln", cell = 1568, hdg = 180}, {"Hawk ln", cell = 1080, hdg = 60}, {"Hawk ln", cell = 1440, hdg = 120 }, {"Hawk ln", cell = 1588, hdg = 180}, -- a supply truck or three {"M 818", number = {min=4, max=6}, random=true}, --AAA defense {"Vulcan", number = {min=2, max=3}, random=true}, --IR defense {"M1097 Avenger", random=true}, }, description = "Hawk SAM site", groupName = "Hawk" }, }, { aliases = {"patriot", "pa", "mim-104"}, group = { disposition = { h= 7, w= 12}, units = { {"Patriot str", cell = 66, hdg = 0}, {"Patriot cp" , cell = 78}, {"Patriot AMG", cell = 79}, {"Patriot ECS", cell = 67}, {"Patriot EPP", cell = 68}, {"Patriot ln", cell = 1, hdg = 280}, {"Patriot ln", cell = 2, hdg = 300}, {"Patriot ln", cell = 28, hdg = 20}, {"Patriot ln", cell = 29, hdg = 40}, {"Patriot ln", cell = 32, hdg = 330}, {"Patriot ln", cell = 33, hdg = 310}, {"Patriot ln", cell = 11, hdg = 60}, {"Patriot ln", cell = 12, hdg = 80}, --supply trucks {"M 818", number = {min=2, max=5}, random=true}, --AAA defense {"Vulcan", number = {min=0, max=1}, cell = 60}, {"Vulcan", number = {min=0, max=1}, cell = 48}, {"Vulcan", cell = 54}, }, description = "Patriot SAM site", groupName = "Patriot" }, }, { -- blue ewr position aliases = {"blue_ewr"}, group = { disposition = {h= 7, w= 7}, units = { -- Radar unit {"FPS-117 Dome", cell = 25}, -- IR Defense {"M1097 Avenger", random=true} }, description = "Blue EWR", groupName = "Blue EWR" }, }, --infantry { aliases = {"infantry section", "infsec"}, group = { disposition = { h= 10, w= 4}, units = {{"IFV BTR-80", cell=38, random=true},{"IFV BTR-80", cell=39, random=true},{"INF Soldier AK", number = {min=12, max=30}, random=true}, {"SA-18 Igla manpad", number = {min=0, max=2}, random=true}}, description = "Mechanized infantry section with APCs", groupName = "Mechanized infantry section" }, }, { aliases = {"US infgroup"}, group = { disposition = { h = 5, w = 5}, units = {{"Hummer", number = {min=1, max=2}, random=true},{"Soldier M249", number = {min=1, max=2}, random=true},{"Soldier M4", number = {min=2, max=4}, random=true},{"Soldier M4 GRG", number = {min=6, max=15}, random=true}}, description = "US infantry group", groupName = "US infantry group", }, }, --artillery { aliases = {"insurgent_arty"}, group = { disposition = { h = 2, w = 4}, units = { {"tt_B8M1", number = {min=1, max=2}}, {"HL_B8M1", number = {min=1, max=2}} }, description = "Insurgent artillery battery", groupName = "Insurgent artillery battery", }, }, { aliases = {"mortar"}, group = { disposition = { h = 2, w = 4}, units = { {"2B11 mortar", number = 4} }, description = "2B11 Mortar team", groupName = "2B11 Mortar team", }, }, { aliases = {"M-109"}, group = { disposition = { h = 4, w = 4}, units = { {"M-109", number = 8}, {"MLRS FDDM", number = 1}, {"M 818", number = 1} }, description = "M-109 artillery battery", groupName = "M-109 artillery battery", }, }, { aliases = {"PLZ05"}, group = { disposition = { h = 2, w = 3}, units = { {"PLZ05", number = 3}, {"Grad_FDDM", number = 1}, {"Ural-375", number = 1} }, description = "PLZ05 artillery battery", groupName = "PLZ05 artillery battery", }, }, { aliases = {"Msta"}, group = { disposition = { h = 2, w = 3}, units = { {"SAU Msta", number = 3}, {"Grad_FDDM", number = 1}, {"Ural-375", number = 1} }, description = "Msta artillery battery", groupName = "Msta artillery battery", }, }, { aliases = {"MLRS"}, group = { disposition = { h = 2, w = 4}, units = { {"MLRS", number = 4}, {"MLRS FDDM", number = 1}, {"M 818", number = 1} }, description = "M270 MLRS artillery battery", groupName = "M270 MLRS artillery battery", }, }, { aliases = {"SmerchCM"}, group = { disposition = { h = 2, w = 4}, units = { {"Smerch", number = 4}, {"Grad_FDDM", number = 1}, {"ZIL-135", number = 2} }, description = "Smerch (CM) MLRS artillery battery", groupName = "Smerch (CM) MLRS artillery battery", }, }, { aliases = {"SmerchHE"}, group = { disposition = { h = 2, w = 4}, units = { {"Smerch_HE", number = 4}, {"Grad_FDDM", number = 1}, {"ZIL-135", number = 2} }, description = "Smerch (HE) MLRS artillery battery", groupName = "Smerch (HE) MLRS artillery battery", }, }, { aliases = {"Uragan"}, group = { disposition = { h = 2, w = 4}, units = { {"Uragan_BM-27", number = 4}, {"Grad_FDDM", number = 1}, {"ZIL-135", number = 2} }, description = "Uragan MLRS artillery battery", groupName = "Uragan MLRS artillery battery", }, }, { aliases = {"Grad"}, group = { disposition = { h = 2, w = 4}, units = { {"Grad-URAL", number = 4}, {"Grad_FDDM", number = 1}, {"Ural-375", number = 2} }, description = "Grad MLRS artillery battery", groupName = "Grad MLRS artillery battery", }, }, --convoys { aliases = {"US supply convoy","blueconvoy"}, group = { disposition = { h = 20, w = 20}, units = { {"Hummer", number = {min=2, max=4}, random=true}, {"Truck M 818", number = {min=3, max=6}, random=true}, {"Truck M978 HEMTT Tanker", number = {min=0, max=3}, random=true}, {"Truck Predator GCS", number = {min=0, max=2}, random=true}, {"Truck Predator TrojanSpirit", number = {min=0, max=2}, random=true} }, description = "US supply convoy", groupName = "US supply convoy", }, }, { aliases = {"RU supply convoy with defense","redconvoy-def"}, group = { disposition = { h = 20, w = 20}, units = { {"2S6 Tunguska", number = {min=0, max=1}, random=true}, {"Strela-10M3", number = {min=0, max=1}, random=true}, {"Strela-1 9P31", number = {min=0, max=1}, random=true}, {"ZSU-23-4 Shilka", number = {min=0, max=2}, random=true}, {"ZSU_57_2", number = {min=0, max=1}, random=true}, {"S-60_Type59_Artillery", number = {min=0, max=1}, random=true}, {"Ural-375 ZU-23", number = {min=0, max=2}, random=true}, {"UAZ-469", number = {min=2, max=4}, random=true}, {"Truck SKP-11", number = {min=1, max=3}, random=true}, {"Ural-375 PBU", number = {min=1, max=3}, random=true}, {"Truck Ural-375", number = {min=1, max=3}, random=true}, {"Truck Ural-4320 APA-5D", number = {min=1, max=3}, random=true}, {"Truck Ural-4320-31", number = {min=1, max=3}, random=true}, {"Truck Ural-4320T", number = {min=1, max=3}, random=true}, {"Truck ZiL-131 APA-80", number = {min=1, max=3}, random=true}, {"Truck ZIL-131 KUNG", number = {min=1, max=3}, random=true} }, description = "RU supply convoy with defense", groupName = "RU supply convoy with defense", }, }, { aliases = {"RU supply convoy with light defense","redconvoy-lightdef"}, group = { disposition = { h = 20, w = 20}, units = { {"ZSU-23-4 Shilka", number = {min=0, max=2}, random=true}, {"ZSU_57_2", number = {min=0, max=1}, random=true}, {"S-60_Type59_Artillery", number = {min=0, max=1}, random=true}, {"Ural-375 ZU-23", number = {min=0, max=2}, random=true}, {"UAZ-469", number = {min=2, max=4}, random=true}, {"Truck SKP-11", number = {min=1, max=3}, random=true}, {"Ural-375 PBU", number = {min=1, max=3}, random=true}, {"Truck Ural-375", number = {min=1, max=3}, random=true}, {"Truck Ural-4320 APA-5D", number = {min=1, max=3}, random=true}, {"Truck Ural-4320-31", number = {min=1, max=3}, random=true}, {"Truck Ural-4320T", number = {min=1, max=3}, random=true}, {"Truck ZiL-131 APA-80", number = {min=1, max=3}, random=true}, {"Truck ZIL-131 KUNG", number = {min=1, max=3}, random=true} }, description = "RU supply convoy with light defense", groupName = "RU supply convoy with light defense", }, }, { aliases = {"RU supply convoy with no defense","redconvoy-nodef"}, group = { disposition = { h = 20, w = 20}, units = { {"UAZ-469", number = {min=2, max=4}, random=true}, {"Truck SKP-11", number = {min=1, max=3}, random=true}, {"Ural-375 PBU", number = {min=1, max=3}, random=true}, {"Truck Ural-375", number = {min=1, max=3}, random=true}, {"Truck Ural-4320 APA-5D", number = {min=1, max=3}, random=true}, {"Truck Ural-4320-31", number = {min=1, max=3}, random=true}, {"Truck Ural-4320T", number = {min=1, max=3}, random=true}, {"Truck ZiL-131 APA-80", number = {min=1, max=3}, random=true}, {"Truck ZIL-131 KUNG", number = {min=1, max=3}, random=true} }, description = "RU supply convoy with no defense", groupName = "RU supply convoy with no defense", }, }, { aliases = {"RU small supply convoy with defense","redsmallconvoy-def"}, group = { disposition = { h = 20, w = 20}, units = { {"2S6 Tunguska", number = {min=0, max=1}, random=true}, {"Strela-10M3", number = {min=0, max=1}, random=true}, {"Strela-1 9P31", number = {min=0, max=1}, random=true}, {"ZSU-23-4 Shilka", number = {min=0, max=2}, random=true}, {"ZSU_57_2", number = {min=0, max=1}, random=true}, {"S-60_Type59_Artillery", number = {min=0, max=1}, random=true}, {"Ural-375 ZU-23", number = {min=0, max=2}, random=true}, {"UAZ-469", number = {min=1, max=2}, random=true}, {"Truck SKP-11", number = {min=1, max=2}, random=true}, {"Ural-375 PBU", number = {min=0, max=2}, random=true}, {"Truck Ural-375", number = {min=0, max=2}, random=true}, {"Truck Ural-4320 APA-5D", number = {min=0, max=2}, random=true}, {"Truck Ural-4320-31", number = {min=0, max=2}, random=true}, {"Truck Ural-4320T", number = {min=0, max=2}, random=true}, {"Truck ZiL-131 APA-80", number = {min=0, max=2}, random=true}, {"Truck ZIL-131 KUNG", number = {min=0, max=2}, random=true} }, description = "RU small supply convoy with defense", groupName = "RU small supply convoy with defense", }, }, { aliases = {"RU small supply convoy with light defense","redsmallconvoy-lightdef"}, group = { disposition = { h = 20, w = 20}, units = { {"ZSU-23-4 Shilka", number = {min=0, max=2}, random=true}, {"ZSU_57_2", number = {min=0, max=1}, random=true}, {"S-60_Type59_Artillery", number = {min=0, max=1}, random=true}, {"Ural-375 ZU-23", number = {min=0, max=2}, random=true}, {"UAZ-469", number = {min=1, max=2}, random=true}, {"Truck SKP-11", number = {min=1, max=2}, random=true}, {"Ural-375 PBU", number = {min=0, max=2}, random=true}, {"Truck Ural-375", number = {min=0, max=2}, random=true}, {"Truck Ural-4320 APA-5D", number = {min=0, max=2}, random=true}, {"Truck Ural-4320-31", number = {min=0, max=2}, random=true}, {"Truck Ural-4320T", number = {min=0, max=2}, random=true}, {"Truck ZiL-131 APA-80", number = {min=0, max=2}, random=true}, {"Truck ZIL-131 KUNG", number = {min=0, max=2}, random=true} }, description = "RU small supply convoy with light defense", groupName = "RU small supply convoy with light defense", }, }, { aliases = {"RU small supply convoy with no defense","redsmallconvoy-nodef","redconvoy","convoy"}, group = { disposition = { h = 20, w = 20}, units = { {"UAZ-469", number = {min=1, max=2}, random=true}, {"Truck SKP-11", number = {min=1, max=2}, random=true}, {"Ural-375 PBU", number = {min=0, max=2}, random=true}, {"Truck Ural-375", number = {min=0, max=2}, random=true}, {"Truck Ural-4320 APA-5D", number = {min=0, max=2}, random=true}, {"Truck Ural-4320-31", number = {min=0, max=2}, random=true}, {"Truck Ural-4320T", number = {min=0, max=2}, random=true}, {"Truck ZiL-131 APA-80", number = {min=0, max=2}, random=true}, {"Truck ZIL-131 KUNG", number = {min=0, max=2}, random=true} }, description = "RU small supply convoy with no defense", groupName = "RU small supply convoy with no defense", }, }, { -- High value offensive convoy potentially defended by Sa-15, Sa-19, Sa-13 and armor aliases = {"hv_convoy_red"}, group = { disposition = { h= 4, w= 4}, units = { --Radar defense {"Tor 9A331", number = {min=0, max=1}, random=true, hdg=0}, {"2S6 Tunguska", number = {min=0, max=1}, random=true, hdg=0}, -- scud units {"Scud_B", number = {min=0, max=2}, random=true, hdg=0}, -- armor {"T-72B3", random=true, hdg=0}, {"BTR-82A", random=true, hdg=0}, --supply truck and C2 {"ZIL-135", random=true, hdg=0}, {"Tigr_233036", random=true, hdg=0}, --IR defense {"Strela-10M3", number = {min=0, max=1}, random=true, hdg=0} }, description = "High Value Attack convoy red", groupName = "High Value Attack convoy red" }, }, { -- Offensive convoy potentially defended by Sa-15, Shilka, Sa-13 and armor aliases = {"attack_convoy_red"}, group = { disposition = { h= 4, w= 4}, units = { --Radar defense {"Tor 9A331", number = {min=0, max=1}, random=true}, {"ZSU-23-4 Shilka", random=true}, -- armor {"T-72B3", random=true}, {"BTR-82A", random=true}, --supply truck {"ZIL-135", random=true}, --IR defense {"Strela-10M3", random=true} }, description = "Attack convoy red", groupName = "Attack convoy red" }, }, { -- Quick reaction convoy potentially defended by Roland and armor aliases = {"QRC_red"}, group = { disposition = { h= 4, w= 4}, units = { --Radar defense {"Roland ADS", number = {min=0, max=1}, random=true}, -- armor {"BTR-82A", number = {min=0, max=1}, random=true}, {"BTR-82A", random=true}, --ATGM {"VAB_Mephisto", random=true}, {"VAB_Mephisto", random=true}, }, description = "Quick reaction convoy red", groupName = "Quick reaction convoy red" }, }, { -- Offensive convoy potentially defended by Sa-15, Sa-19, Sa-13 and armor aliases = {"civilian_convoy_red"}, group = { disposition = {h= 3, w= 3}, units = { -- buses {"LAZ Bus", number = {min=1,max=3}, random=true}, {"LiAZ Bus", number = {min=1,max=3}, random=true}, }, description = "Civilian convoy red", groupName = "Civilian convoy red" }, }, { -- Quick reaction convoy potentially defended by Roland and armor aliases = {"QRC_blue"}, group = { disposition = { h= 4, w= 4}, units = { --Radar defense {"Roland ADS", number = {min=0, max=1}, random=true}, -- armor {"M1128 Stryker MGS", number = {min=0, max=1}, random=true}, {"M1128 Stryker MGS", random=true}, --ATGM {"M1134 Stryker ATGM", random=true}, {"M1134 Stryker ATGM", random=true}, }, description = "Quick reaction convoy blue", groupName = "Quick reaction convoy blue" }, }, --ships { aliases = {"cargoships-nodef", "cargoships"}, group = { disposition = { h = 20, w = 20}, units = { {"Dry-cargo ship-1", number = {min=1, max=3}, random=true, size=150}, {"Dry-cargo ship-2", number = {min=1, max=3}, random=true, size=150}, {"ELNYA", number = {min=1, max=3}, random=true, size=150} }, description = "Cargo ships with no defense", groupName = "Cargo ships with no defense", }, }, { aliases = {"cargoships-escorted"}, group = { disposition = { h = 20, w = 20}, units = { {"Dry-cargo ship-1", number = {min=1, max=3}, random=true, size=150}, {"Dry-cargo ship-2", number = {min=1, max=3}, random=true, size=150}, {"ELNYA", number = {min=1, max=3}, random=true, size=150}, {"MOLNIYA", number = {min=1, max=2}, random=true, size=150}, {"ALBATROS", number = {min=1, max=2}, random=true, size=150}, {"NEUSTRASH", number = {min=0, max=1}, random=true, size=150} }, description = "Cargo ships with escort", groupName = "Cargo ships with escort", }, }, { aliases = {"combatships"}, group = { disposition = { h = 20, w = 20}, units = { {"MOLNIYA", number = {min=2, max=3}, random=true, size=150}, {"ALBATROS", number = {min=2, max=3}, random=true, size=150}, {"NEUSTRASH", number = {min=1, max=2}, random=true, size=150} }, description = "Combat ships with possible FFG defense", groupName = "Combat ships", }, }, --- --- groups made for dynamic group spawning (veafCasMission.generateAirDefenseGroup) --- { aliases = {"generateAirDefenseGroup-BLUE-5"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- hawk battery {"Hawk sr", cell = 8}, {"Hawk pcp", cell = 13}, {"Hawk tr", cell = 15}, {"Hawk ln", cell = 1, hdg = 225}, {"Hawk ln", cell = 3, hdg = 0 }, {"Hawk ln", cell = 21, hdg = 135}, -- Some M48 Chaparral {"M48 Chaparral", number = {min=2, max=4}, random=true}, -- Some Gepards {"Gepard", number = {min=2, max=4}, random=true}, -- a supply truck or three {"M 818", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-BLUE-5", groupName = "generateAirDefenseGroup-BLUE-5", }, }, { aliases = {"generateAirDefenseGroup-BLUE-4"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- Roland battery {"Roland Radar", random=true}, {"Roland ADS", random=true, hdg = 0}, {"Roland ADS", random=true, hdg = 225}, {"Roland ADS", random=true, hdg = 135}, -- Some M48 Chaparral {"M48 Chaparral", number = {min=2, max=4}, random=true}, -- Some Gepards {"Gepard", number = {min=2, max=4}, random=true}, -- a supply truck or three {"M 818", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-BLUE-4", groupName = "generateAirDefenseGroup-BLUE-4", }, }, { aliases = {"generateAirDefenseGroup-BLUE-3"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- Some Gepards {"Gepard", number = {min=2, max=4}, random=true}, -- M6 Linebacker battery {"M6 Linebacker", hdg = 0, random=true}, {"M6 Linebacker", hdg = 90, random=true}, {"M6 Linebacker", hdg = 180, random=true}, {"M6 Linebacker", hdg = 270, random=true}, -- Some M1097 Avenger {"M1097 Avenger", number = {min=2, max=4}, random=true}, -- a supply truck or three {"M 818", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-BLUE-3", groupName = "generateAirDefenseGroup-BLUE-3", }, }, { aliases = {"generateAirDefenseGroup-BLUE-2"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- Some Vulcans {"Vulcan", number = {min=2, max=4}, random=true}, -- Some M1097 Avenger {"M1097 Avenger", number = {min=2, max=4}, random=true}, -- a supply truck or three {"M 818", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-BLUE-2", groupName = "generateAirDefenseGroup-BLUE-2", }, }, { aliases = {"generateAirDefenseGroup-BLUE-1"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- Some Vulcans {"Vulcan", number = {min=1, max=3}, random=true}, -- Some M1097 Avenger {"M1097 Avenger", number = {min=0, max=1}, random=true}, -- a supply truck or three {"M 818", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-BLUE-1", groupName = "generateAirDefenseGroup-BLUE-1", }, }, { aliases = {"generateAirDefenseGroup-BLUE-0"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- Some AAV7 {"AAV7", number = {min=1, max=3}, random=true}, -- a supply truck or three {"M 818", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-BLUE-0", groupName = "generateAirDefenseGroup-BLUE-0", }, }, { aliases = {"generateAirDefenseGroup-RED-5"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- the search radar {"Dog Ear radar", random=true}, -- Tor battery {"Tor 9A331", hdg = 180, number = 1, random=true}, -- SA-8 battery {"Osa 9A33 ln", number = {min=1, max=2}, random=true}, -- Some SA13 {"Strela-10M3", number = {min=1, max=2}, random=true}, -- Some Tunguskas {"2S6 Tunguska", number = {min=1, max=2}, random=true}, -- Some ZU-57-2 {"ZSU_57_2", number = 1, random=true}, -- Some S-60 {"S-60_Type59_Artillery", number = 1, random=true}, -- a supply truck or three {"Ural-4320-31", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-RED-5", groupName = "generateAirDefenseGroup-RED-5", }, }, { aliases = {"generateAirDefenseGroup-RED-4"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- the search radar {"Dog Ear radar", random=true}, -- SA-8 battery {"Osa 9A33 ln", number = {min=1, max=2}, random=true}, -- Some SA13 {"Strela-10M3", number = {min=1, max=2}, random=true}, -- Some Shilkas {"ZSU-23-4 Shilka", number = {min=1, max=2}, random=true}, -- Some ZU-57-2 {"ZSU_57_2", number = {min=1, max=1}, random=true}, -- Some S-60 {"S-60_Type59_Artillery", number = {min=0, max=1}, random=true}, -- a supply truck or three {"Ural-4320-31", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-RED-4", groupName = "generateAirDefenseGroup-RED-4", }, }, { aliases = {"generateAirDefenseGroup-RED-3"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- the search radar {"Dog Ear radar", random=true}, -- SA13 battery {"Strela-10M3", number = {min=1, max=2}, random=true}, -- Some SA9 {"Strela-1 9P31", number = {min=1, max=2}, random=true}, -- Some Shilkas {"ZSU-23-4 Shilka", number = {min=1, max=2}, random=true}, -- Some ZU-57-2 {"ZSU_57_2", number = {min=0, max=1}, random=true}, -- Some S-60 {"S-60_Type59_Artillery", number = {min=0, max=1}, random=true}, -- a supply truck or three {"Ural-4320-31", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-RED-3", groupName = "generateAirDefenseGroup-RED-3", }, }, { aliases = {"generateAirDefenseGroup-RED-2"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- the search radar {"Dog Ear radar", random=true}, -- SA9 battery {"Strela-1 9P31", number = {min=1, max=2}, random=true}, -- Some Shilkas {"ZSU-23-4 Shilka", number = {min=1, max=2}, random=true}, -- Some ZU-57-2 {"ZSU_57_2", number = {min=0, max=1}, random=true}, -- Some S-60 {"S-60_Type59_Artillery", number = {min=0, max=1}, random=true}, -- a supply truck or three {"Ural-4320-31", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-RED-2", groupName = "generateAirDefenseGroup-RED-2", }, }, { aliases = {"generateAirDefenseGroup-RED-1"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- the search radar {"Dog Ear radar", random=true}, -- Some Shilkas {"ZSU-23-4 Shilka", number = {min=1, max=2}, random=true}, -- Some ZU-57-2 {"ZSU_57_2", number = {min=0, max=1}, random=true}, -- a supply truck or three {"Ural-4320-31", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-RED-1", groupName = "generateAirDefenseGroup-RED-1", }, }, { aliases = {"generateAirDefenseGroup-RED-0"}, hidden = true, group = { disposition = { h= 7, w= 7}, units = { -- Some Ural-375 ZU-23 {"Ural-375 ZU-23", number = {min=0, max=1}, random=true}, -- Some S-60 {"S-60_Type59_Artillery", number = {min=0, max=1}, random=true}, -- a supply truck or three {"Ural-4320-31", number = {min=1, max=3}, random=true} }, description = "generateAirDefenseGroup-RED-0", groupName = "generateAirDefenseGroup-RED-0", }, }, --- --- Seemingly realistic russian air defense batteries --- { aliases = {"RU-SAM-Shilka-Battery", "shilka-battery"}, group = { disposition = { h= 5, w= 5}, units = { -- the search radar {"Dog Ear radar", cell = 13}, -- the actual air defense units {"ZSU-23-4 Shilka", hdg = 0, random=true}, {"ZSU-23-4 Shilka", hdg = 90, random=true}, {"ZSU-23-4 Shilka", hdg = 180, random=true}, {"ZSU-23-4 Shilka", hdg = 270, random=true}, -- a supply truck or three {"Ural-4320-31", number = {min=1, max=3}, random=true}, }, description = "ZSU-23-4 battery", groupName = "ZSU-23-4 battery" }, }, { aliases = {"RU-SAM-S60-Battery", "s60-battery"}, group = { disposition = { h= 5, w= 5}, units = { -- the search radar {"Dog Ear radar", cell = 13}, -- the actual air defense units {"S-60_Type59_Artillery", hdg = 0, random=true}, {"S-60_Type59_Artillery", hdg = 90, random=true}, {"S-60_Type59_Artillery", hdg = 180, random=true}, {"S-60_Type59_Artillery", hdg = 270, random=true}, -- a supply truck or three {"Ural-4320-31", number = {min=1, max=3}, random=true} }, description = "S-60 battery", groupName = "S-60 battery" }, }, { aliases = {"RU-SAM-SA9-Battery", "sa9-battery"}, group = { disposition = { h= 5, w= 5}, units = { -- the search radar {"Dog Ear radar", cell = 13}, -- the actual air defense units {"Strela-1 9P31", hdg = 0, random=true}, {"Strela-1 9P31", hdg = 90, random=true}, {"Strela-1 9P31", hdg = 180, random=true}, {"Strela-1 9P31", hdg = 270, random=true}, -- a supply truck or three {"Ural-4320-31", number = {min=1, max=3}, random=true} }, description = "SA-9 battery", groupName = "SA-9 battery" }, }, { aliases = {"RU-SAM-SA13-Battery", "sa13-battery"}, group = { disposition = { h= 5, w= 5}, units = { -- the search radar {"Dog Ear radar", cell = 13}, -- the actual air defense units {"Strela-10M3", hdg = 0, random=true}, {"Strela-10M3", hdg = 90, random=true}, {"Strela-10M3", hdg = 180, random=true}, {"Strela-10M3", hdg = 270, random=true}, -- a supply truck or three {"Ural-4320-31", number = {min=1, max=3}, random=true}, }, description = "SA-13 battery", groupName = "SA-13 battery" }, }, { aliases = {"US-refuel", "refuel-group"}, group = { disposition = { h= 1, w= 4}, units = { {"M978 HEMTT Tanker", cell = 1, fitToUnit = true}, {"M1043 HMMWV Armament", cell = 2, fitToUnit = true}, {"Truck M939 Heavy", cell = 3, fitToUnit = true}, {"Diesel Power Station 5I57A", cell = 4, fitToUnit = true}, }, description = "US refuel group", groupName = "US refuel group" }, }, } veaf.loggers.get(veafUnits.Id):info(string.format("Loading version %s", veafUnits.Version)) if veafUnits.OutputListsForDocumentation then veafUnits.logGroupsListInMarkdown() veafUnits.logUnitsListInMarkdown() end ------------------ END script veafUnits.lua ------------------ ------------------ START script veafGroundAI.lua ------------------ ------------------------------------------------------------------ -- VEAF Ground AI (a.k.a. Slightly Less Dumb Ground AI) for DCS World -- By Zip (2024-25) -- -- Features: -- --------- -- * DCS groups can be managed by the mission maker (API calls, radio menus) and by the pilots (radio menus, markers, remote commands) -- -- See the documentation : https://veaf.github.io/documentation/mission-maker/groundAI.html ------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafGroundAI = {} --- Identifier. All output in the log will start with this. veafGroundAI.Id = "GROUNDAI - " --- Version. veafGroundAI.Version = "1.0.0" -- trace level, specific to this module --veafGroundAI.LogLevel = "trace" --- Key phrase to look for in the mark text which triggers the spawn command. veafGroundAI.MarkerKeyphrase = "_ground" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veaf.loggers.new(veafGroundAI.Id, veafGroundAI.LogLevel) veafGroundAI.handlers = {} veafGroundAI.WATCHDOG_DELAY = 1 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- GroundUnitHandler class ------------------------------------------------------------------------------------------------------------------------------------------------------------- GroundUnitHandler = {} GroundUnitHandler.CLASS_NAME = "GroundUnitHandler" GroundUnitHandler.DEFAULT_MESSAGE_STOP = "Ground unit %s has stopped executing and awaiting orders." GroundUnitHandler.DEFAULT_MESSAGE_START = "Ground unit %s is executing or awaiting orders." function GroundUnitHandler.init(object) -- technical name (GroundUnitHandler instance name) object.name = nil -- draw the position and orders of the unit on screen object.draw = false -- player units (only they are concerned by the messages) object.playerUnitsNames = {} -- DCS group object.dcsGroup = nil -- orders for the ground unit object.orders = {} -- index of the currently executed order object.currentOrderIndex = 1 -- silent means no message is emitted object.silent = false -- the drawing objects that has been used to draw the situation object.zoneDrawings = {} -- the scheduled state of the :check() function object.checkFunctionSchedule = nil -- status, from one of the GroundUnitHandler.STATUS_xxx constants object.status = GroundUnitHandler.STATUS_READY -- message when the ground unit starts executing orders object.messageStart = GroundUnitHandler.DEFAULT_MESSAGE_START -- event when the ground unit starts executing orders object.onStart = nil -- message when the ground unit stops executing orders object.messageStop = GroundUnitHandler.DEFAULT_MESSAGE_STOP -- event when the ground unit stops executing orders object.onStop = nil end function GroundUnitHandler.statusToString(status) if status == GroundUnitHandler.STATUS_READY then return "STATUS_READY" end if status == GroundUnitHandler.STATUS_ACTIVE then return "STATUS_ACTIVE" end if status == GroundUnitHandler.STATUS_OVER then return "STATUS_OVER" end return "" end GroundUnitHandler.STATUS_READY = 1 GroundUnitHandler.STATUS_ACTIVE = 2 GroundUnitHandler.STATUS_OVER = 4 function GroundUnitHandler:new(objectToCopy) veaf.loggers.get(veafGroundAI.Id):debug(GroundUnitHandler.CLASS_NAME .. ":new()") local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object GroundUnitHandler.init(objectToCreate) return objectToCreate end -- technical name (GroundUnitHandler instance name) function GroundUnitHandler:setName(value) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[]:setName(%s)", veaf.p(value)) self.name = value return veafGroundAI.add(self) -- add the handler to the list as soon as a name is available to index it end -- technical name (GroundUnitHandler instance name) function GroundUnitHandler:getName() return self.name or self.description end -- description for the messages function GroundUnitHandler:getDescription() local result = self:getName() if self:getDcsGroup() then result = result .. " is handling DCS group " .. self:getDcsGroup():getName() .. ")" end return result end -- draw the position and orders of the unit on screen function GroundUnitHandler:setDraw(value) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:setDraw(%s)", veaf.p(self:getName()), veaf.p(value)) self.draw = value return self end -- draw the position and orders of the unit on screen function GroundUnitHandler:getDraw() return self.draw end -- coalitions of the players (only human units from these coalitions will be monitored) function GroundUnitHandler:setPlayerCoalitions(value) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:setPlayerCoalitions(%s)", veaf.p(self:getName()), veaf.p(value)) self.playerCoalitions = value return self end -- player units (only they are concerned by the messages) function GroundUnitHandler:setPlayerUnitsNames(value) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:setPlayerUnitsNames(%s)", veaf.p(self:getName()), veaf.p(value)) self.playerUnitsNames = value return self end -- player units (only they are concerned by the messages) function GroundUnitHandler:getPlayerUnitsNames() return self.playerUnitsNames end -- DCS group function GroundUnitHandler:setDcsGroup(value) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:setDcsGroup(%s)", veaf.p(self:getName()), veaf.p(value)) self.dcsGroup = value return self end -- DCS group function GroundUnitHandler:getDcsGroup() return self.dcsGroup end -- current orders for the ground unit function GroundUnitHandler:setOrders(value) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:setOrders(%s)", veaf.p(self:getName()), veaf.p(value)) self.orders = value return self end -- orders for the ground unit function GroundUnitHandler:addOrder(value) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:addOrder(%s)", veaf.p(self:getName()), veaf.p(value)) if value then table.insert(self.orders, value) end return self end -- orders for the ground unit function GroundUnitHandler:getOrders() return self.orders end -- orders for the ground unit function GroundUnitHandler:clearOrders() veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:clearOrders()", veaf.p(self:getName())) self.orders = {} return self end -- get the current order function GroundUnitHandler:getCurrentOrder() if self.orders then return self.orders[1] else return nil end end -- complete an order (pop it from the start of the list) function GroundUnitHandler:completeOrder() veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:completeOrder()", veaf.p(self:getName())) if self.orders and #self.orders > 0 then table.remove(self.orders, 1) end return self end -- silent means no message is emitted function GroundUnitHandler:setSilent(value) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:setSilent(%s)", veaf.p(self:getName()), veaf.p(value)) self.silent = value return self end -- silent means no message is emitted function GroundUnitHandler:getSilent() return self.silent end -- the drawing objects that has been used to draw the situation function GroundUnitHandler:setZoneDrawings(value) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:setZoneDrawings(%s)", veaf.p(self:getName()), veaf.p(value)) self.zoneDrawings = value return self end -- the drawing objects that has been used to draw the situation function GroundUnitHandler:getZoneDrawings() return self.zoneDrawings end -- the scheduled state of the :check() function function GroundUnitHandler:setCheckFunctionSchedule(value) --veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME.."[%s]:setCheckFunctionSchedule(%s)", veaf.p(self:getName()), veaf.p(value)) self.checkFunctionSchedule = value return self end -- the scheduled state of the :check() function function GroundUnitHandler:getCheckFunctionSchedule() return self.checkFunctionSchedule end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- METHODS function GroundUnitHandler:handleOrder(order) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:handleOrder(%s)", veaf.p(self:getName()),veaf.p(order)) -- do nothing clever, all is done in the inheriting classes self:completeOrder() end function GroundUnitHandler:check() --veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME.."[%s]:check()", veaf.p(self:getName())) -- consider the orders in the orders list local currentOrder = self:getCurrentOrder() if currentOrder then -- do something with the order self:handleOrder(currentOrder) end -- reschedule the check function self:setCheckFunctionSchedule(mist.scheduleFunction(GroundUnitHandler.check, { self }, timer.getTime() + veafGroundAI.WATCHDOG_DELAY)) end function GroundUnitHandler:start() veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:start()", veaf.p(self:getName())) self.status = GroundUnitHandler.STATUS_ACTIVE if not self.silent then trigger.action.outText(string.format(self.messageStart, self:getName()), 10) end if self.onStart then self.onStart(self) end self:check() end function GroundUnitHandler:stop() veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:stop()", veaf.p(self:getName())) self.status = GroundUnitHandler.STATUS_READY if not self.silent then trigger.action.outText(string.format(self.messageStop, self:getName()), 10) end if self.onStop then self.onStop(self) end if self.checkFunctionSchedule then mist.removeFunction(self.checkFunctionSchedule) self.checkFunctionSchedule = nil end if self:getCheckFunctionSchedule() then mist.removeFunction(self:getCheckFunctionSchedule()) self:setCheckFunctionSchedule(nil) end end function GroundUnitHandler:orderTextAnalysis(value) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:orderTextAnalysis(%s)", veaf.p(self:getName()), veaf.p(value)) -- do nothing clever, all is done in the inheriting classes end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ArtilleryUnitHandler class ------------------------------------------------------------------------------------------------------------------------------------------------------------- ArtilleryUnitHandler = GroundUnitHandler:new() ArtilleryUnitHandler.CLASS_NAME = "ArtilleryUnitHandler" -- fire for aim constants ArtilleryUnitHandler.FIREFORAIM_SHELLS = 2 ArtilleryUnitHandler.FIREFORAIM_RADIUS = 10 -- fire for effect constants ArtilleryUnitHandler.FIREFOREFFECT_SHELLS = 40 ArtilleryUnitHandler.FIREFOREFFECT_RADIUS = 100 ArtilleryUnitHandler.ORDER_STOP = 0 ArtilleryUnitHandler.ORDER_FIRE = 1 ArtilleryUnitHandler.ORDER_ADVANCE = 2 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- CTOR function ArtilleryUnitHandler.init(object) -- status, from one of the ArtilleryUnitHandler.STATUS_xxx constants object.status = ArtilleryUnitHandler.STATUS_READY end function ArtilleryUnitHandler:new(objectToCopy) veaf.loggers.get(veafGroundAI.Id):debug(ArtilleryUnitHandler.CLASS_NAME .. ":new()") local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object ArtilleryUnitHandler.init(objectToCreate) return objectToCreate end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- PROPERTIES ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- COMPUTED PROPERTIES ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- METHODS function ArtilleryUnitHandler:orderTextAnalysis(text) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:orderTextAnalysis(%s)", veaf.p(self:getName()), veaf.p(text)) -- analyze the string for an acceptable order ArtilleryUnitHandler.VERB_FIRE_FORAIM = 1 ArtilleryUnitHandler.VERB_FIRE_FOREFFECT = 2 -- Option parameters extracted from the mark text. local options = {} options.verb = ArtilleryUnitHandler.VERB_FIRE_FORAIM -- can be "aim", "fire" options.target = nil -- the coordinates of the target options.shells = nil -- the number of shells to fire options.radius = nil -- the precision of the shelling -- Check for correct keywords. if text:lower():find("aim") then options.verb = ArtilleryUnitHandler.VERB_FIRE_FORAIM elseif text:lower():find("fire") then options.verb = ArtilleryUnitHandler.VERB_FIRE_FOREFFECT else return nil end -- keywords are split by ";" local keywords = veaf.split(text, ";") for _, keyphrase in pairs(keywords) do -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. local str = veaf.breakString(veaf.trim(keyphrase), " ") local key = str[1] local val = str[2] or "" if key:lower() == "target" then -- Set the target veaf.loggers.get(veafGroundAI.Id):trace("Keyword target = %s", veaf.p(val)) if veaf.computeLLFromString(val) then -- check target string validity options.target = val end end if key:lower() == "shells" then -- Set the number of shells veaf.loggers.get(veafGroundAI.Id):trace("Keyword shells = %s", veaf.p(val)) local nVal = veaf.getRandomizableNumeric(val) veaf.loggers.get(veafGroundAI.Id):trace("shells = %s", veaf.p(nVal)) options.shells = nVal end if key:lower() == "radius" then -- Set the radius of the shelling veaf.loggers.get(veafGroundAI.Id):trace("Keyword radius = %s", veaf.p(val)) local nVal = veaf.getRandomizableNumeric(val) veaf.loggers.get(veafGroundAI.Id):trace("radius = %s", veaf.p(nVal)) options.radius = nVal end end if options.verb == ArtilleryUnitHandler.VERB_FIRE_FORAIM then self:fireForAim(options.target, options.shells, options.radius) elseif options.verb == ArtilleryUnitHandler.VERB_FIRE_FOREFFECT then self:fireForEffect(options.target, options.shells, options.radius) end return options end -- give the artillery unit a fire for effect order function ArtilleryUnitHandler:fireForAim(coordinates, shells, radius) if not shells then shells = ArtilleryUnitHandler.FIREFORAIM_SHELLS end if not radius then radius = ArtilleryUnitHandler.FIREFORAIM_RADIUS end veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:fireForAim(%s, %s, %s)", veaf.p(self:getName()), veaf.p(coordinates), veaf.p(shells), veaf.p(radius)) -- check the parameters if not coordinates then veaf.loggers.get(veafGroundAI.Id):warn(self.CLASS_NAME .. "[%s]:fireForAim() : no target coordinates", veaf.p(self:getName())) if not self.silent then local message = string.format("%s cannot aim, no target coordinates provided", veaf.p(self:getName())) trigger.action.outText(message, 10) end return end self:fireAtCoordinates(coordinates, shells, radius) end -- give the artillery unit a fire for effect order function ArtilleryUnitHandler:fireForEffect(coordinates, shells, radius) if not shells then shells = ArtilleryUnitHandler.FIREFOREFFECT_SHELLS end if not radius then radius = ArtilleryUnitHandler.FIREFOREFFECT_RADIUS end if not coordinates then coordinates = self._lastTarget end veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:fireForEffect(%s, %s)", veaf.p(self:getName()), veaf.p(shells), veaf.p(radius)) if not coordinates then veaf.loggers.get(veafGroundAI.Id):warn(self.CLASS_NAME .. "[%s]:fireForEffect() : no previous target - cannot fire for effect", veaf.p(self:getName())) if not self.silent then local message = string.format("%s cannot fire for effect, no target coordinates provided and no previous target exist", veaf.p(self:getName())) trigger.action.outText(message, 10) end return end self:fireAtCoordinates(coordinates, shells, radius) end -- give the artillery unit a fire order function ArtilleryUnitHandler:fireAtCoordinates(coordinates, shells, radius) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:fireAtCoordinates(%d, %s, %s)", veaf.p(self:getName()), veaf.p(shells), veaf.p(coordinates), veaf.p(radius)) -- check the parameters if not shells then veaf.loggers.get(veafGroundAI.Id):warn(self.CLASS_NAME .. "[%s]:fireAtCoordinates() : shells is nil", veaf.p(self:getName())) return end if not coordinates then veaf.loggers.get(veafGroundAI.Id):warn(self.CLASS_NAME .. "[%s]:fireAtCoordinates() : coordinates is nil", veaf.p(self:getName())) return end if not radius then radius = ArtilleryUnitHandler.DEFAULT_FIRE_RADIUS end -- check if these are coordinates local target = nil if type(coordinates) == "table" then target = coordinates elseif type(coordinates) == "string" then local _lat, _lon = veaf.computeLLFromString(coordinates) veaf.loggers.get(veafGroundAI.Id):trace(string.format("_lat=%s", veaf.p(_lat))) veaf.loggers.get(veafGroundAI.Id):trace(string.format("_lon=%s", veaf.p(_lon))) if _lat and _lon then target = coord.LLtoLO(_lat, _lon) else veaf.loggers.get(veafGroundAI.Id):warn(self.CLASS_NAME .. "[%s]:fireAtCoordinates() : coordinates are not valid: %s", veaf.p(self:getName()), veaf.p(coordinates)) end end local order = { verb = ArtilleryUnitHandler.ORDER_FIRE, parameters = { shells = shells, target = target, radius = radius } } self:addOrder(order) end function ArtilleryUnitHandler:handleOrder(order) veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:handleOrder(%s)", veaf.p(self:getName()), veaf.p(order)) if order.verb == ArtilleryUnitHandler.ORDER_FIRE then -- fire at the target local shells = order.parameters.shells local target = order.parameters.target local radius = order.parameters.radius if not target then veaf.loggers.get(veafGroundAI.Id):warn(self.CLASS_NAME .. "[%s]:handleOrder() : no target", veaf.p(self:getName())) else -- convert the target coordinates to UTM for the message local lat, lon, _ = coord.LOtoLL(target) local grid = coord.LLtoMGRS(lat, lon) local coordinates = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing local message = string.format("%s is firing %d shells at %s with a %s m dispersion", veaf.p(self:getName()), veaf.p(shells), veaf.p(coordinates), veaf.p(radius)) trigger.action.outText(message, 10) veaf.loggers.get(veafGroundAI.Id):trace("ArtilleryUnitHandler[%s]:handleOrder() : firing %d shells at %s with a %s m dispersion", veaf.p(self:getName()), veaf.p(shells), veaf.p(coordinates), veaf.p(radius)) -- fire the shells local fireParams = { x = target.x, y = target.z, zoneRadius = radius, expendQty = shells, expendQtyEnabled = true, counterbattaryRadius = 500, } local fire = { id = 'FireAtPoint', params = fireParams } self:getDcsGroup():getController():pushTask(fire) self._lastTarget = target end end self:completeOrder() end function ArtilleryUnitHandler:stop() veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:stop()", veaf.p(self:getName())) -- clear the group's orders queue self:getDcsGroup():getController():resetTask() return GroundUnitHandler.stop(self) end function ArtilleryUnitHandler:clearOrders() veaf.loggers.get(veafGroundAI.Id):debug(self.CLASS_NAME .. "[%s]:clearOrders()", veaf.p(self:getName())) -- clear the group's orders queue self:getDcsGroup():getController():resetTask() return GroundUnitHandler.clearOrders(self) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function executed when a mark has changed. This happens when text is entered or changed. function veafGroundAI.onEventMarkChange(eventPos, event) -- choose by default the coalition of the player who triggered the event local coa = coalition.side.BLUE if event.coalition == coalition.side.RED then coa = coalition.side.RED end veaf.loggers.get(veafGroundAI.Id):trace(string.format("event.idx = %s", veaf.p(event.idx))) if veafGroundAI.executeCommand(eventPos, event.text, coa, event.idx) then -- Delete old mark. veaf.loggers.get(veafGroundAI.Id):trace(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafGroundAI.executeCommand(eventPos, eventText, eventCoalition, markId, bypassSecurity, spawnedGroups, route) veaf.loggers.get(veafGroundAI.Id):debug(string.format("veafGroundAI.executeCommand(eventText=[%s])", eventText)) -- Check if marker has a text and contains an alias if eventText ~= nil then -- Analyse the mark point text and extract the keywords. local options = veafGroundAI.markTextAnalysis(eventPos, eventCoalition, eventText) veaf.loggers.get(veafGroundAI.Id):trace(string.format("options = %s", veaf.p(options))) if options then -- do the magic if options.verb == veafGroundAI.VERB_SET then veaf.loggers.get(veafGroundAI.Id):trace("options.verb == veafGroundAI.VERB_SET") local handlerName = options.name local group = options.group if group and handlerName then veaf.loggers.get(veafGroundAI.Id):trace("group = %s", veaf.p(group)) local handler = veafGroundAI.get(handlerName) if not handler then handler = ArtilleryUnitHandler:new():setName(handlerName) end if handler then handler:setDcsGroup(group) handler:start() return true end end elseif options.verb == veafGroundAI.VERB_UNSET then veaf.loggers.get(veafGroundAI.Id):trace("options.verb == veafGroundAI.VERB_UNSET") local handlerName = options.name local handler = veafGroundAI.get(handlerName) if handler then handler:stop() veafGroundAI.remove(handler) return true end elseif options.verb == veafGroundAI.VERB_START then veaf.loggers.get(veafGroundAI.Id):trace("options.verb == veafGroundAI.VERB_START") local handlerName = options.name local handler = veafGroundAI.get(handlerName) if handler then handler:start() return true end elseif options.verb == veafGroundAI.VERB_STOP then veaf.loggers.get(veafGroundAI.Id):trace("options.verb == veafGroundAI.VERB_STOP") local handlerName = options.name local handler = veafGroundAI.get(handlerName) if handler then handler:stop() return true end elseif options.verb == veafGroundAI.VERB_CLEAR then veaf.loggers.get(veafGroundAI.Id):trace("options.verb == veafGroundAI.VERB_CLEAR") local handlerName = options.name local handler = veafGroundAI.get(handlerName) if handler then handler:stop() handler:clearOrders() return true end elseif options.verb == veafGroundAI.VERB_STATUS then veaf.loggers.get(veafGroundAI.Id):trace("options.verb == veafGroundAI.VERB_STATUS") local handlerName = options.name local handler = veafGroundAI.get(handlerName) if handler then trigger.action.outText(string.format("AI handler %s: %s", handlerName, handler:getDescription()), 10) return true end elseif options.verb == veafGroundAI.VERB_ORDER then veaf.loggers.get(veafGroundAI.Id):trace("options.verb == veafGroundAI.VERB_ORDER") local handlerName = options.name local handler = veafGroundAI.get(handlerName) if handler then if handler:orderTextAnalysis(options.order) then return true end end end end end -- None of the keywords matched. return false end --- Extract keywords from mark text. function veafGroundAI.markTextAnalysis(eventPos, eventCoalition, text) veaf.loggers.get(veafGroundAI.Id):trace("veafGroundAI.markTextAnalysis(text=%s)", veaf.p(text)) veafGroundAI.VERB_SET = 1 veafGroundAI.VERB_UNSET = 2 veafGroundAI.VERB_ORDER = 3 veafGroundAI.VERB_START = 4 veafGroundAI.VERB_STOP = 5 veafGroundAI.VERB_CLEAR = 6 veafGroundAI.VERB_STATUS = 7 -- Option parameters extracted from the mark text. local options = {} options.verb = veafGroundAI.VERB_SET -- can be "set", "unset", "order", "start", "stop", "status" options.group = nil -- the DCS group that is concerned by "set" and "unset" verbs options.order = nil -- the order that is given by "order" verb options.name = nil -- the name of the handler that is concerned by all verbs -- Check for correct keywords. if text:lower():find(veafGroundAI.MarkerKeyphrase .. " set") then options.verb = veafGroundAI.VERB_SET elseif text:lower():find(veafGroundAI.MarkerKeyphrase .. " unset") then options.verb = veafGroundAI.VERB_UNSET elseif text:lower():find(veafGroundAI.MarkerKeyphrase .. " order") then options.verb = veafGroundAI.VERB_ORDER elseif text:lower():find(veafGroundAI.MarkerKeyphrase .. " start") then options.verb = veafGroundAI.VERB_START elseif text:lower():find(veafGroundAI.MarkerKeyphrase .. " stop") then options.verb = veafGroundAI.VERB_STOP elseif text:lower():find(veafGroundAI.MarkerKeyphrase .. " clear") then options.verb = veafGroundAI.VERB_CLEAR elseif text:lower():find(veafGroundAI.MarkerKeyphrase .. " status") then options.verb = veafGroundAI.VERB_STATUS else return nil end -- keywords are split by "," local keywords = veaf.split(text, ",") for _, keyphrase in pairs(keywords) do -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. local str = veaf.breakString(veaf.trim(keyphrase), " ") local key = str[1] local val = str[2] or "" if key:lower() == "groupname" then -- Set dcs group name. veaf.loggers.get(veafGroundAI.Id):trace("Keyword groupname = %s", veaf.p(val)) -- search for the DCS group options.group = Group.getByName(val) end if key:lower() == "name" then -- Set AI handler name. veaf.loggers.get(veafGroundAI.Id):trace("Keyword name = %s", veaf.p(val)) options.name = val end if key:lower() == "order" then -- Set order veaf.loggers.get(veafGroundAI.Id):trace("Keyword order = %s", veaf.p(val)) options.order = val end end -- check mandatory parameter "name" for all commands if not (options.name) then return nil end -- check mandatory parameter "groupname" for commands "set" and "unset" if ((options.verb == veafGroundAI.VERB_SET or options.verb == veafGroundAI.VERB_UNSET) and not (options.group)) then -- search for the nearest allied group local minDist = 999999 local closestUnit = nil for _, unit in pairs(veaf.getUnitsOfCoalition(false, eventCoalition)) do local pos = unit:getPosition().p if pos then local name = unit:getName() local distanceFromCenter = ((pos.x - eventPos.x) ^ 2 + (pos.z - eventPos.z) ^ 2) ^ 0.5 veaf.loggers.get(veaf.Id):trace("name=%s; distanceFromCenter=%s", veaf.p(name), veaf.p(distanceFromCenter)) if distanceFromCenter <= 250 then if distanceFromCenter < minDist then minDist = distanceFromCenter closestUnit = unit end end end end if closestUnit then options.group = closestUnit:getGroup() else return nil end end return options end ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Global functions for the module ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafGroundAI.add(handler) veaf.loggers.get(veafGroundAI.Id):debug("veafGroundAI.add([%s])", veaf.p(handler:getName())) veafGroundAI.handlers[handler:getName():lower()] = handler return handler end function veafGroundAI.remove(handler) veaf.loggers.get(veafGroundAI.Id):debug("veafGroundAI.remove([%s])", veaf.p(handler:getName())) veafGroundAI.handlers[handler:getName():lower()] = nil end function veafGroundAI.get(handlerName) veaf.loggers.get(veafGroundAI.Id):debug("veafGroundAI.get([%s])", veaf.p(handlerName)) local handler = veafGroundAI.handlers[handlerName:lower()] if handler then veaf.loggers.get(veafGroundAI.Id):trace("handler found: %s", veaf.p(handler)) end return handler end function veafGroundAI.initialize() veaf.loggers.get(veafGroundAI.Id):info("Initializing module") veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafGroundAI.onEventMarkChange) end ------------------ END script veafGroundAI.lua ------------------ ------------------ START script veafWeather.lua ------------------ ------------------------------------------------------------------ -- VEAF weather information messages and markers -- By Flogas (2024) -- -- Features: -- --------- -- * Generation of weather messages and reports in different formats (METAR, ATIS) -- * Generation of markers on the maps displaying the weather at the location ------------------------------------------------------------------ veafWeather = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global module settings ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafWeather.Id = "WEATHER" --- Version. veafWeather.Version = "1.5.1" -- trace level, specific to this module --veafWeather.LogLevel = "trace" veaf.loggers.new(veafWeather.Id, veafWeather.LogLevel) --- Key phrase to look for in the mark text which triggers the command. veafWeather.Keyphrase = "_weather" veafWeather.RadioMenuName = "WEATHER AND ATC" veafWeather.RemoteCommandParser = "([[a-zA-Z0-9]+)%s?([^%s]*)%s?(.*)" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Local constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- local _dcsPresetDensity = { -- {density, precipitation, visibility} ["Preset1"] = {2, false, nil}, -- LS1 -- FEW/SCT ["Preset2"] = {2, false, nil}, -- LS2 -- FEW/SCT ["Preset3"] = {3, false, nil}, -- HS1 -- SCT ["Preset4"] = {3, false, nil}, -- HS2 -- SCT ["Preset5"] = {3, false, nil}, -- S1 -- SCT ["Preset6"] = {4, false, nil}, -- S2 -- SCT/BKN ["Preset7"] = {3, false, nil}, -- S3 -- BKN ["Preset8"] = {4, false, nil}, -- HS3 -- SCT/BKN ["Preset9"] = {5, false, nil}, -- S4 -- BKN ["Preset10"] = {4, false, nil}, -- S5 -- SCT/BKN ["Preset11"] = {6, false, nil}, -- S6 -- BKN ["Preset12"] = {6, false, nil}, -- S7 -- BKN ["Preset13"] = {6, false, nil}, -- B1 -- BKN ["Preset14"] = {6, false, nil}, -- B2 -- BKN ["Preset15"] = {4, false, nil}, -- B3 -- SCT/BKN ["Preset16"] = {6, false, nil}, -- B4 -- BKN ["Preset17"] = {7, false, nil}, -- B5 -- BKN/OVC ["Preset18"] = {7, false, nil}, -- B6 -- BKN/OVC ["Preset19"] = {8, false, nil}, -- B7 -- OVC ["Preset20"] = {7, false, nil}, -- B8 -- BKN/OVC ["Preset21"] = {7, false, nil}, -- O1 -- BKN/OVC ["Preset22"] = {6, false, nil}, -- O2 -- BKN ["Preset23"] = {6, false, nil}, -- O3 -- BKN ["Preset24"] = {7, false, nil}, -- O4 -- BKN/OVC ["Preset25"] = {8, false, nil}, -- O5 -- OVC ["Preset26"] = {8, false, nil}, -- O6 -- OVC ["Preset27"] = {8, false, nil}, -- O7 -- OVC ["RainyPreset1"] = {8, true, 4000}, -- OR1 -- OVC ["RainyPreset2"] = {7, true, 3000}, -- OR2 -- BKN/OVC ["RainyPreset3"] = {8, true, 4000}, -- OR3 -- OVC ["RainyPreset4"] = {4, true, nil}, -- LR1 -- SCT/BKN ["RainyPreset5"] = {7, true, nil}, -- LR2 -- BKN/OVC ["RainyPreset6"] = {8, true, nil}, -- LR3 -- OVC ["NEWRAINPRESET4"] = {8, true, nil}, -- LR4 -- OVC } local _cloudDensity = { Clear = 0, Few = 1, Scattered = 2, Broken = 3, Overcast = 4 } local _cloudDensityOktas = { [0] = _cloudDensity.Clear, [1] = _cloudDensity.Few, [2] = _cloudDensity.Few, [3] = _cloudDensity.Scattered, [4] = _cloudDensity.Scattered, [5] = _cloudDensity.Broken, [6] = _cloudDensity.Broken, [7] = _cloudDensity.Overcast, [8] = _cloudDensity.Overcast } local _nKelvinToCelciusOffset = -273.15 local _visibilityAffect = { None = 0, Fog = 1, Mist = 2, Haze = 3 } ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Local tools ------------------------------------------------------------------------------------------------------------------------------------------------------------- local function _computeClearSkyHumidity(nLatitude, nLongitude, iDayOfYear) -- Base RH for clear skies local baseRh = 30 -- Seasonal adjustment -- Calculate seasonal factor (-1 to 1, peaks at middle of year) local seasonal_factor = math.cos((iDayOfYear - 182) * 2 * math.pi / 365) -- Latitude adjustment -- Higher latitudes tend to have lower RH in winter, higher in summer local lat_factor = math.abs(nLatitude) / 90 -- Longitude adjustment (crude estimation of continental vs maritime) -- Assume areas around 0°, 180° longitude (typical ocean areas) have higher RH local long_factor = math.min(math.abs(nLongitude), math.abs(nLongitude - 180)) / 180 local maritime_influence = 1 - long_factor -- Combine factors local seasonal_adjustment = seasonal_factor * 15 * lat_factor -- ±15% variation local maritime_adjustment = maritime_influence * 10 -- Up to +10% for maritime areas -- Calculate final RH local adjustedRh = baseRh + seasonal_adjustment + maritime_adjustment -- Clamp between reasonable values return math.max(20, math.min(70, adjustedRh)) end local function _computeHumidity(vec3, iCloudBaseMeters, iVisibilityMeters, bPrecipitations, iAbsTime) local nLatitude, nLongitude, _ = coord.LOtoLL(vec3) local nHumidity if (iCloudBaseMeters == nil or iCloudBaseMeters > 10000) then -- Clear skies - estimate RH based on location and date local dateTime = veafTime.getMissionDateTime(iAbsTime) nHumidity = _computeClearSkyHumidity(nLatitude, nLongitude, dateTime.yday) else -- Convert cloud base to meters and estimate RH nHumidity = 100 - (iCloudBaseMeters / 100) -- Clamp RH between 0 and 100% nHumidity = math.max(0, math.min(100, nHumidity)) end if (iVisibilityMeters < 1000) then nHumidity = 100 -- Fog implies saturation elseif (iVisibilityMeters < 5000) then nHumidity = math.max(nHumidity, 90) -- At least 90% RH with any fog elseif (iVisibilityMeters < 10000) then -- Increase RH as visibility decreases local nVisibilityFactor = math.max(0, (10000 - iVisibilityMeters) / 10000) nHumidity = nHumidity + (nVisibilityFactor * 20) -- Up to +20% for low visibility end -- Precipitation adjustments if (bPrecipitations) then nHumidity = math.max(nHumidity, 80) end -- Clamp final value return math.max(0, math.min(100, nHumidity)) end local function _computeDewpoint(nTemperatureCelcius, nQnhPa, nHumidity) local nQnhHpa = nQnhPa / 100 -- Constants for Magnus formula local a = 17.27 local b = 237.7 -- Calculate gamma term local gamma = ((a * nTemperatureCelcius) / (b + nTemperatureCelcius)) + math.log(nHumidity/100.0) -- Calculate dew point using Magnus formula local nDewPointCelcius = (b * gamma) / (a - gamma) -- Apply pressure correction (approximate) local pressure_correction = (1013.25 - nQnhHpa) * 0.0012 nDewPointCelcius = nDewPointCelcius + pressure_correction nDewPointCelcius = math.min(nDewPointCelcius, nTemperatureCelcius) -- Round to one decimal place return math.floor(nDewPointCelcius * 10 + 0.5) / 10 end local function _weatherSliceAtAltitude(vec3, iAltitudeMeters) local nTemperatureKelvin, nPressurePa = atmosphere.getTemperatureAndPressure({ x = vec3.x, y = iAltitudeMeters, z = vec3.z }) local iWindDir, iWindSpeedMps = weathermark._GetWind(vec3, iAltitudeMeters) return { AltitudeMeters = iAltitudeMeters, PressureHpa = nPressurePa / 100, TemperatureCelcius = nTemperatureKelvin + _nKelvinToCelciusOffset, WindDirection = iWindDir, WindSpeedMps = iWindSpeedMps } end local function _getFlightLevelString(iAltitudeFeet) -- Round to nearest 500 local iAltitudeFeetRounded = math.floor((iAltitudeFeet + 250) / 500) * 500 -- Convert to flight level format (divide by 100) local iFlightLevel = math.floor(iAltitudeFeetRounded / 100) return string.format("FL%03d", iFlightLevel) end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- Weather measurement unit systems class --- Defines a set of units to be used to display weather data --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veafWeatherUnitSystem = {} veafWeatherUnitSystem.__index = veafWeatherUnitSystem veafWeatherUnitSystem.Units = { Kts = 0, Mps = 1, M = 2, Sm = 3, Nm = 4, Ft = 5, Hpa = 6, InHg = 7, MmHg = 8 } --------------------------------------------------------------------------------------------------- --- CTOR function veafWeatherUnitSystem:create(windSpeeds, visibilities, altitudes, pressures) local this = { WindSpeeds = windSpeeds, Visibilities = visibilities, Altitudes = altitudes, Pressures = pressures, } setmetatable(this, veafWeatherUnitSystem) return this end --------------------------------------------------------------------------------------------------- --- Static data veafWeatherUnitSystem.Systems = { Full = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Kts, veafWeatherUnitSystem.Units.Mps }, { veafWeatherUnitSystem.Units.M, veafWeatherUnitSystem.Units.Sm, veafWeatherUnitSystem.Units.Nm }, { veafWeatherUnitSystem.Units.Ft, veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.Hpa, veafWeatherUnitSystem.Units.InHg, veafWeatherUnitSystem.Units.MmHg }), -- all Icao = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Kts }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.Ft }, { veafWeatherUnitSystem.Units.Hpa }), -- default IcaoMetric = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Mps }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.Ft }, { veafWeatherUnitSystem.Units.Hpa }), -- for russian airfields Faa = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Kts }, { veafWeatherUnitSystem.Units.Sm }, { veafWeatherUnitSystem.Units.Ft }, { veafWeatherUnitSystem.Units.InHg }), -- for US aircrafts or airfields, and for older british aircrafts FaaMetric = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Kts }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.Ft }, { veafWeatherUnitSystem.Units.InHg }), -- for US army helicopters FaaNavy = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Kts }, { veafWeatherUnitSystem.Units.Nm }, { veafWeatherUnitSystem.Units.Ft }, { veafWeatherUnitSystem.Units.InHg }), -- for US aircraft carriers Metric = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Mps }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.Hpa }), -- for french army helicopters MetricEastern = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Mps }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.MmHg }), -- for russian and chinese aircrafts } veafWeatherUnitSystem.DefaultUnitSystem = veafWeatherUnitSystem.Systems.Icao veafWeatherUnitSystem.Theatres = {} veafWeatherUnitSystem.Theatres.Faa = { veaf.theatreName.Nevada, veaf.theatreName.MarianaIslands } veafWeatherUnitSystem.Theatres.IcaoMetric = { veaf.theatreName.Caucasus } veafWeatherUnitSystem.Aircrafts = {} veafWeatherUnitSystem.Aircrafts.Faa = { "A-10A", "A-10C", "A-10C_2", "AV8BNA", "F-14A-135-GR", "F-14B", "F-15C", "F-15ESE", "F-16C_50", "FA-18C_hornet", "F-4E-45MC", "UH-1H", "P-47D-30", "P-47D-40", "P-51D", "P-51D-30-NA", "TF-51D", "Christen Eagle II", "SpitfireLFMkIX", "MosquitoFBMkVI" } veafWeatherUnitSystem.Aircrafts.Metric = { "SA342L", "SA342M", "SA342Minigun", "SA342Mistral" } veafWeatherUnitSystem.Aircrafts.MetricEastern = { "Ka-50", "Ka-50_3", "Mi-8MTV2", "Mi-24P", "MiG-15bis", "MiG-19P", "MiG-21Bis", "MiG-29S", "Su-25", "Su-25T", "Su-27", "Su-33", "J-11A", "FW-190A8", "FW-190D9", "I-16", "L-39C", "L-39ZA", "Yak-52" } veafWeatherUnitSystem.Aircrafts.FaaMetric = { "AH-64D_BLK_II" } --------------------------------------------------------------------------------------------------- --- Methods --- function veafWeatherUnitSystem.defaultForTypeName(sTypeName) if (veaf.tableContains(veafWeatherUnitSystem.Aircrafts.Faa, sTypeName)) then return veafWeatherUnitSystem.Systems.Faa elseif (veaf.tableContains(veafWeatherUnitSystem.Aircrafts.Metric, sTypeName)) then return veafWeatherUnitSystem.Systems.Metric elseif (veaf.tableContains(veafWeatherUnitSystem.Aircrafts.MetricEastern, sTypeName)) then return veafWeatherUnitSystem.Systems.MetricEastern elseif (veaf.tableContains(veafWeatherUnitSystem.Aircrafts.FaaMetric, sTypeName)) then return veafWeatherUnitSystem.Systems.FaaMetric else return veafWeatherUnitSystem.DefaultUnitSystem end end function veafWeatherUnitSystem.defaultForTheatre() local sTheatre = env.mission.theatre if (veaf.tableContains(veafWeatherUnitSystem.Theatres.Faa, sTheatre)) then return veafWeatherUnitSystem.Systems.Faa elseif (veaf.tableContains(veafWeatherUnitSystem.Theatres.IcaoMetric, sTheatre)) then return veafWeatherUnitSystem.Systems.IcaoMetric else return veafWeatherUnitSystem.DefaultUnitSystem end end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- Weather management class --- Collects and compile wheather data form various sources in the sim at a location --- Can be output to string as METAR or ATIS informations --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veafWeatherData = {} veafWeatherData.__index = veafWeatherData --------------------------------------------------------------------------------------------------- --- CTOR function veafWeatherData:create(vec3, iAbsTime, iAltitudeMeters) iAbsTime = iAbsTime or timer.getAbsTime() iAltitudeMeters = iAltitudeMeters or veaf.getLandHeight(vec3) local sunTimesZulu = veafTime.getSunTimesZulu(vec3) local sunTimesLocal = veafTime.getSunTimesLocal(vec3) local iWindDirSurface, iWindSpeedSurfaceMps = weathermark._GetWind(vec3, iAltitudeMeters + 10) -- Measure the wind velocity at the standard height of 10 metres above the surface. This is the internationally accepted meteorological definition of ‘surface wind’ designed to eliminate distortion attributable to very local terrain effects -- the static env.mission.weather.visibility.distance is not used anymore, and the DCS engine apparently sets a default at 100000, as seen in this log line: -- WEATHER (Main): set fog: visibility:100000 thickness:0.0 local iVisibilityMeters = 100000 local clouds = nil local bPrecipitation = false; local sCloudPreset = env.mission.weather.clouds.preset if (veaf.isNullOrEmpty(sCloudPreset)) then if (env.mission.weather.clouds.density > 0) then local iDensity = mist.utils.round(env.mission.weather.clouds.density * 8 / 10) -- 10 levels in dcs, convert to oktas clouds = { Density = iDensity, BaseMeters = env.mission.weather.clouds.base } end bPrecipitation = (env.mission.weather.clouds.iprecptns > 0) else if (_dcsPresetDensity[sCloudPreset]) then clouds = {Density = _dcsPresetDensity[sCloudPreset][1], BaseMeters = env.mission.weather.clouds.base} bPrecipitation = _dcsPresetDensity[sCloudPreset][2] --[[ -- visibilities in presets were already not reliable, and they have no meaning with the new fog visiblity setting if (_dcsPresetDensity[sCloudPreset][3] and _dcsPresetDensity[sCloudPreset][3] < iVisibilityMeters) then iVisibilityMeters = _dcsPresetDensity[sCloudPreset][3] end ]] end end local iFogThicknessMeters = world.weather.getFogThickness() local iFogVisibilityMeters = world.weather.getFogVisibilityDistance() if (iFogThicknessMeters >= iAltitudeMeters) then iVisibilityMeters = iFogVisibilityMeters veaf.loggers.get(veafWeather.Id):trace("Visibility new fog=%d", iVisibilityMeters) else veaf.loggers.get(veafWeather.Id):trace("Visibility=%d. New fog ignored, measure point above fog ceiling [ altitude=%d, fog thickness=%d ]", iVisibilityMeters, iAltitudeMeters, iFogThicknessMeters) end local _, nQfePa = atmosphere.getTemperatureAndPressure({ x = vec3.x, y = iAltitudeMeters, z = vec3.z }) local nTemperatureKelvin, nQnhPa = atmosphere.getTemperatureAndPressure({ x = vec3.x, y = 0, z = vec3.z }) local nTemperatureCelcius = nTemperatureKelvin + _nKelvinToCelciusOffset local nBaseMeters = clouds and clouds.BaseMeters or 99999 local nHumidity = _computeHumidity(vec3, nBaseMeters, iVisibilityMeters, bPrecipitation, iAbsTime) local nDewPointCelcius = _computeDewpoint(nTemperatureCelcius, nQnhPa, nHumidity) -- Fog FG or mist BR: fog is less than 1000 meters visibility. Mist BR or haze HZ: if the humidity is more than 80% it is mist. local visibilityAffect = _visibilityAffect.None if (iVisibilityMeters < 1000) then visibilityAffect = _visibilityAffect.Fog elseif (iVisibilityMeters < 5000 and nHumidity >= 80) then visibilityAffect = _visibilityAffect.Mist elseif (iVisibilityMeters < 5000) then visibilityAffect = _visibilityAffect.Haze end local weatherSlices = { } if (iAltitudeMeters < 600) then table.insert(weatherSlices, _weatherSliceAtAltitude(vec3, 500)) end if (iAltitudeMeters < 2100) then table.insert(weatherSlices, _weatherSliceAtAltitude(vec3, 2000)) end if (iAltitudeMeters < 8100) then table.insert(weatherSlices, _weatherSliceAtAltitude(vec3, 8000)) end local this = { AbsTime = iAbsTime, Vec3 = vec3, AltitudeMeter = iAltitudeMeters, WindDirection = iWindDirSurface, WindSpeedMps = iWindSpeedSurfaceMps, VisibilityMeters = iVisibilityMeters, Dust = env.mission.weather.enable_dust, VisibilityAffect = visibilityAffect, Clouds = clouds, Precipitation = bPrecipitation, TemperatureCelcius = nTemperatureCelcius, DewPointCelcius = nDewPointCelcius, QnhHpa = nQnhPa / 100, QfeHpa = nQfePa / 100, SunriseZulu = sunTimesZulu.Sunrise, SunsetZulu = sunTimesZulu.Sunset, SunriseLocal = sunTimesLocal.Sunrise, SunsetLocal = sunTimesLocal.Sunset, WeatherSlices = weatherSlices, } setmetatable(this, veafWeatherData) veaf.loggers.get(veafWeather.Id):trace(this:toString(veafWeatherUnitSystem.Systems.Faa)) veaf.loggers.get(veafWeather.Id):trace(this:toString(veafWeatherUnitSystem.Systems.FaaNavy)) veaf.loggers.get(veafWeather.Id):trace(this:toString(veafWeatherUnitSystem.Systems.Icao)) return this end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Static methods function veafWeatherData.getWeatherString(vec3, dcsElementName, unitSystem) local bWithLaste = false local sTypeName = veaf.getDcsTypeName(dcsElementName) if (unitSystem == nil) then unitSystem = veafWeatherUnitSystem.defaultForTypeName(sTypeName) end if (not veaf.isNullOrEmpty(sTypeName) and veaf.startsWith(sTypeName, "A-10", false)) then bWithLaste = true end local weatherData = veafWeatherData:create(vec3) return weatherData:toString(unitSystem, bWithLaste) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Methods function veafWeatherData:getNormalizedWindDirection(iDirectionTrue, bMagnetic) bMagnetic = bMagnetic or false local iDirection = iDirectionTrue if (bMagnetic) then iDirection = iDirection - veaf.getMagneticDeclination() if (iDirection) < 0 then iDirection = iDirection + 360 end end if (iDirection == 0) then iDirection = 360 end return iDirection end function veafWeatherData:getNormalizedCloudBaseMeters(bHeight) bHeight = bHeight or false if (self.Clouds == nil or self.Clouds.Density <= 0) then return nil else local iCloudBase = self.Clouds.BaseMeters if (bHeight) then iCloudBase = iCloudBase - self.AltitudeMeter end return iCloudBase end end function veafWeatherData:getNormalizedCloudsDensity() if (self.Clouds == nil or self.Clouds.BaseMeters == nil or self.Clouds.Density <= 0) then return _cloudDensity.Clear else return _cloudDensityOktas[self.Clouds.Density] end end function veafWeatherData:isCavok() local iCloudHeightMeters = self:getNormalizedCloudBaseMeters(true) if (iCloudHeightMeters == nil or mist.utils.metersToFeet(iCloudHeightMeters) < 5000) then return false -- no clouds or cloud below 5000 ft else return (self.VisibilityMeters >= 10000 and not self.Precipitation and not self.Dust) end end function veafWeatherData:getCarrierCase() -- Case I departures are flown during the day when weather conditions allow departure under visual flight rules (VFR). The weather minimums are a cloud deck above 3,000 feet and visibility greater than 5 miles -- Case II departures are flown during the day when visual conditions are present at the carrier, but a controlled climb through the clouds is required. The weather minimums are a cloud deck above 1,000 feet and visibility greater than 5 miles. -- Case III departures are flown at night and when weather conditions are below the minimums of 1,000 feet cloud deck and 5 miles visibility local bNight = veafTime.isAeronauticalNight(self.Vec3, self.AbsTime) if (bNight) then return 3 end local iCloudBase = nil if (self.Clouds and self.Clouds.Density > 4) then iCloudBase = self.Clouds.BaseMeters end local iVisibilityCase12 = mist.utils.NMToMeters(5) local iCloudBaseCase1 = mist.utils.feetToMeters(3000) local iCloudBaseCase2 = mist.utils.feetToMeters(1000) --veaf.loggers.get(veaf.Id):trace(string.format("GetCarrierCase - Cloud base=%d feet (need more than 1000 for CASE 2 and 300 for CASE 3) - visibility=%d nm (need more than 5 for CASE 1/2)", iCloudBase or -1, UTILS.MetersToNM (self.VisibilityMeters))) if (self.VisibilityMeters > iVisibilityCase12 and (iCloudBase == nil or iCloudBase > iCloudBaseCase1)) then return 1 elseif (self.VisibilityMeters > iVisibilityCase12 and (iCloudBase == nil or iCloudBase > iCloudBaseCase2)) then return 2 else return 3 end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToStrings function veafWeatherData:appendString(s, sAppend) sAppend = sAppend or "" if (veaf.isNullOrEmpty(s)) then return sAppend elseif (veaf.isNullOrEmpty(sAppend)) then return s else return s .. "|" .. sAppend end end function veafWeatherData:toStringWind(unitSystem, iDirection, nSpeedMps, bMagnetic) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem bMagnetic = bMagnetic or false if (nSpeedMps <= 0.5) then return "calm" end local iDirection = self:getNormalizedWindDirection(iDirection, bMagnetic) local sSpeedKts = string.format("%dkts", mist.utils.mpsToKnots(nSpeedMps)) local sSpeedMps = string.format("%dm/s", nSpeedMps) local sSpeed if(veaf.tableContains(unitSystem.WindSpeeds, veafWeatherUnitSystem.Units.Kts)) then sSpeed = veafWeatherData:appendString(sSpeed, sSpeedKts) end if(veaf.tableContains(unitSystem.WindSpeeds, veafWeatherUnitSystem.Units.Mps)) then sSpeed = veafWeatherData:appendString(sSpeed, sSpeedMps) end local sDegrees if (bMagnetic) then sDegrees = "°M" else sDegrees = "°T" end return string.format("%03d%s @ %s", iDirection, sDegrees, sSpeed) end function veafWeatherData:toStringVisibility(unitSystem, bWithMax) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem local sVisibilityMeters if (self.VisibilityMeters >= 10000) then sVisibilityMeters = "10+km" else local iVisibilityMeters if (self.VisibilityMeters >= 100) then iVisibilityMeters = mist.utils.round(self.VisibilityMeters / 100) * 100 else iVisibilityMeters = mist.utils.round(self.VisibilityMeters / 50) * 50 end sVisibilityMeters = string.format("%dm", iVisibilityMeters) end local sVisibilityStatuteMile local iVisibilityStatuteMile = self.VisibilityMeters * 0.000621371 if (iVisibilityStatuteMile >= 10) then sVisibilityStatuteMile = "10+SM" elseif (iVisibilityStatuteMile >= 1) then sVisibilityStatuteMile = string.format("%dSM", mist.utils.round(iVisibilityStatuteMile)) elseif (iVisibilityStatuteMile >= 0.75) then sVisibilityStatuteMile = "3/4SM" elseif (iVisibilityStatuteMile >= 0.5) then sVisibilityStatuteMile = "1/2SM" elseif (iVisibilityStatuteMile >= 0.25) then sVisibilityStatuteMile = "1/4SM" else sVisibilityStatuteMile = "0SM" end local sVisibilityNauticalMile local iVisibilityNauticalMile = mist.utils.metersToNM(self.VisibilityMeters) if (iVisibilityNauticalMile >= 10) then sVisibilityNauticalMile = "10+NM" elseif (iVisibilityNauticalMile >= 1) then sVisibilityNauticalMile = string.format("%dNM", mist.utils.round(iVisibilityNauticalMile)) else local iVisibilityYards = mist.utils.round((iVisibilityNauticalMile * 2025.37) / 100) * 100 sVisibilityNauticalMile = string.format("%dyds", iVisibilityYards) end local sVisibility if(veaf.tableContains(unitSystem.Visibilities, veafWeatherUnitSystem.Units.M)) then sVisibility = veafWeatherData:appendString(sVisibility, sVisibilityMeters) end if(veaf.tableContains(unitSystem.Visibilities, veafWeatherUnitSystem.Units.Sm)) then sVisibility = veafWeatherData:appendString(sVisibility, sVisibilityStatuteMile) end if(veaf.tableContains(unitSystem.Visibilities, veafWeatherUnitSystem.Units.Nm)) then sVisibility = veafWeatherData:appendString(sVisibility, sVisibilityNauticalMile) end if (self.VisibilityAffect == _visibilityAffect.Fog) then sVisibility = sVisibility .. " - fog" elseif (self.VisibilityAffect == _visibilityAffect.Haze) then sVisibility = sVisibility .. " - haze" elseif (self.VisibilityAffect == _visibilityAffect.Mist) then sVisibility = sVisibility .. " - mist" end if (self.Dust) then sVisibility = sVisibility .. " - dust" end if (self.Precipitation) then sVisibility = sVisibility .. " - precipitations" end return sVisibility end function veafWeatherData:toStringClouds(unitSystem, bHeight) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem bHeight = bHeight or false local cloudDensity = self:getNormalizedCloudsDensity() local iCloudBaseMeters = self:getNormalizedCloudBaseMeters(bHeight) local sCloudDensity = "" local sCloudBase = "" if (cloudDensity == _cloudDensity.Clear) then sCloudDensity = "No clouds" else if (cloudDensity == _cloudDensity.Scattered) then sCloudDensity = "Scattered clouds" elseif (cloudDensity == _cloudDensity.Broken) then sCloudDensity = "Broken clouds" elseif (cloudDensity == _cloudDensity.Overcast) then sCloudDensity = "Overcast clouds" else sCloudDensity = "Few clouds" end if (iCloudBaseMeters ~= nil and iCloudBaseMeters > 0) then local iCloudBaseFeet = math.floor((mist.utils.metersToFeet(iCloudBaseMeters) + 250) / 500) * 500 local iCloudBaseMeters = math.floor((iCloudBaseMeters + 250) / 500) * 500 local sCloudBaseFeet = string.format("%dft", iCloudBaseFeet) local sCloudBaseMeters = string.format("%dm", iCloudBaseMeters) if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.Ft)) then sCloudBase = veafWeatherData:appendString(sCloudBase, sCloudBaseFeet) end if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.M)) then sCloudBase = veafWeatherData:appendString(sCloudBase, sCloudBaseMeters) end sCloudBase = string.format(" @ %s", sCloudBase) if (bHeight) then sCloudBase = sCloudBase .. " AGL" else sCloudBase = sCloudBase .. " ASL" end end end return string.format("%s%s", sCloudDensity, sCloudBase) end function veafWeatherData:toStringTemperature(nTemperatureCelcius) return string.format("%d°C", mist.utils.round(nTemperatureCelcius)) end function veafWeatherData:toStringPressure(unitSystem, nPressureHpa) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem local sPressureHpa = string.format("%.0fHpa", nPressureHpa) local sPressureInHg = string.format("%.2finHg", mist.utils.converter("hpa", "inhg", nPressureHpa)) local sPressureMmHg = string.format("%.0fmmHg", nPressureHpa * 0.75006375541921) -- mist convert has the wrong coefficient for hpa to mmHg local sPressure if(veaf.tableContains(unitSystem.Pressures, veafWeatherUnitSystem.Units.Hpa)) then sPressure = veafWeatherData:appendString(sPressure, sPressureHpa) end if(veaf.tableContains(unitSystem.Pressures, veafWeatherUnitSystem.Units.InHg)) then sPressure = veafWeatherData:appendString(sPressure, sPressureInHg) end if(veaf.tableContains(unitSystem.Pressures, veafWeatherUnitSystem.Units.MmHg)) then sPressure = veafWeatherData:appendString(sPressure, sPressureMmHg) end return sPressure end function veafWeatherData:toStringSunTime(dateTimeZulu, bZulu, bLocal) local sLocal = "" if (bLocal) then local dateTimeLocal = veafTime.toLocal(dateTimeZulu) sLocal = string.format("%sL", veafTime.toStringTime(dateTimeLocal, false)) end local sZulu = "" if (bZulu) then sZulu = string.format("%sZ", veafTime.toStringTime(dateTimeZulu, false)) end if (bLocal and bZulu) then return string.format("%s - %s", sZulu, sLocal) elseif (bLocal) then return sLocal else return sZulu end end function veafWeatherData:toStringSlice(weatherSlice, unitSystem, bMagnetic) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem bMagnetic = bMagnetic or false local sAltitudeMeters = string.format("%dm", weatherSlice.AltitudeMeters) local sAltitudeFl = _getFlightLevelString(mist.utils.metersToFeet(weatherSlice.AltitudeMeters)) local sAltitude if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.Ft)) then sAltitude = veafWeatherData:appendString(sAltitude, sAltitudeFl) end if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.M)) then sAltitude = veafWeatherData:appendString(sAltitude, sAltitudeMeters) end local sTemperature = self:toStringTemperature(weatherSlice.TemperatureCelcius) local sPressure = self:toStringPressure(unitSystem, weatherSlice.PressureHpa) local sWind = self:toStringWind(unitSystem, weatherSlice.WindDirection, weatherSlice.WindSpeedMps, bMagnetic) return string.format("%s: wind %s ; %s", sAltitude, sWind, sTemperature) end function veafWeatherData:toStringLaste() local function _getLasteAt(iDesiredHeightFeet) local iAltitudeFeet = math.floor((mist.utils.metersToFeet(self.AltitudeMeter) + iDesiredHeightFeet + 500) / 1000) * 1000 local iAltitudeMeters = mist.utils.feetToMeters(iAltitudeFeet) local iTemperatureKelvin, _ = atmosphere.getTemperatureAndPressure({ x = self.Vec3.x, y = iAltitudeMeters, z = self.Vec3.z }) local iWindDirection, iWindSpeedMps = weathermark._GetWind(self.Vec3, iAltitudeMeters) local iWindDirectionMagnetic = veafWeatherData:getNormalizedWindDirection(iWindDirection, true) local sLaste = string.format("ALT%02d W%03d/%02d T%+d", iAltitudeFeet / 1000, iWindDirectionMagnetic, mist.utils.mpsToKnots(iWindSpeedMps), iTemperatureKelvin + _nKelvinToCelciusOffset) veaf.loggers.get(veafWeather.Id):trace(string.format("LASTE @ %f - W%dM %dT", iAltitudeFeet, iWindDirectionMagnetic, iWindDirection)) veaf.loggers.get(veafWeather.Id):trace(sLaste) return sLaste end local sLaste = "" sLaste = sLaste .. string.format("\n%s", _getLasteAt(2000)) sLaste = sLaste .. string.format("\n%s", _getLasteAt(8000)) sLaste = sLaste .. string.format("\n%s", _getLasteAt(16000)) --sLaste = sLaste .. string.format("\n%s", _getLasteAt(28000)) return sLaste end function veafWeatherData:toString(unitSystem, bWithLaste) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem bWithLaste = bWithLaste or false local sString = "" sString = sString .. string.format("Wind: %s", self:toStringWind(unitSystem, self.WindDirection, self.WindSpeedMps)) sString = sString .. "\n" sString = sString .. string.format("\nVisibility: %s", self:toStringVisibility(unitSystem)) sString = sString .. string.format("\nClouds: %s", self:toStringClouds(unitSystem, true)) sString = sString .. "\n" sString = sString .. string.format("\nTemperature: %s - Dew point: %s", self:toStringTemperature(self.TemperatureCelcius), self:toStringTemperature(self.DewPointCelcius)) sString = sString .. string.format("\nQNH: %s", self:toStringPressure(unitSystem, self.QnhHpa)) sString = sString .. string.format("\nQFE: %s", self:toStringPressure(unitSystem, self.QfeHpa)) sString = sString .. string.format("\nSunrise: %s", self:toStringSunTime(self.SunriseZulu, true, true)) sString = sString .. string.format("\nSunset: %s", self:toStringSunTime(self.SunsetZulu, true, true)) sString = sString .. "\n" if(bWithLaste) then sString = sString .. string.format("\nLASTE:%s", self:toStringLaste()) else for _, weatherSlice in pairs(self.WeatherSlices) do sString = sString .. string.format("\n @ %s", self:toStringSlice(weatherSlice, unitSystem)) end end return sString end function veafWeatherData:toStringExtended(unitSystem, bHeight) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem local sAltitudeFeet = string.format("%dft", mist.utils.round(mist.utils.metersToFeet(self.AltitudeMeter))) local sAltitudeMeters = string.format("%dm", mist.utils.round(self.AltitudeMeter)) local sAltitude if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.Ft)) then sAltitude = veafWeatherData:appendString(sAltitude, sAltitudeFeet) end if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.M)) then sAltitude = veafWeatherData:appendString(sAltitude, sAltitudeMeters) end local nLatitude, nLongitude = coord.LOtoLL(self.Vec3) local sString = "" sString = sString .. string.format("Time: %s", veafTime.absTimeToStringDateTime(self.AbsTime)) sString = sString .. string.format("\nLocation: %s", mist.tostringLL(nLatitude, nLongitude, 0, true)) sString = sString .. string.format("\nAltitude: %s", sAltitude) sString = sString .. "\n\n" .. self:toString(unitSystem, bHeight) return sString end function veafWeatherData:toStringAtis(unitSystem) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem local sAtis = "" sAtis = sAtis .. string.format("Wind %s", self:toStringWind(unitSystem, self.WindDirection, self.WindSpeedMps, true)) if(self:isCavok()) then sAtis = sAtis .. "\nCeiling and visiblity OK, CAVOK" else sAtis = sAtis .. string.format("\nVisibility %s, %s", self:toStringVisibility(unitSystem), self:toStringClouds(unitSystem, true)) end sAtis = sAtis .. string.format("\nTemperature %s, dew point %s", self:toStringTemperature(self.TemperatureCelcius), self:toStringTemperature(self.DewPointCelcius)) sAtis = sAtis .. string.format("\nQNH %s", self:toStringPressure(unitSystem, self.QnhHpa)) if(veafTime.isAeronauticalNight(self.Vec3, self.AbsTime)) then sAtis = sAtis .. string.format("\nSunrise %s", self:toStringSunTime(self.SunriseZulu, true, false)) else sAtis = sAtis .. string.format("\nSunset %s", self:toStringSunTime(self.SunsetZulu, true, false)) end return sAtis end --[[ function FgWeather:ToStringMetar() local iWindForce, iWindDirection = self:GetFormattedWind() local sWind if (iWindForce < 1) then sWind = "00000KT" else iWindDirection = UTILS.Round(iWindDirection / 10) * 10 if (iWindDirection == 0) then iWindDirection = 360 end sWind = string.format("%03d%02dKT", iWindDirection, iWindForce) end local iVisibility = UTILS.Round(self.VisibilityMeters / 100) * 100 if (iVisibility >= 10000) then iVisibility = 9999 end local sVisibility = string.format("%04d", iVisibility) local sSignificativeWeather = nil if (self.Precipitation) then sSignificativeWeather = "RA" -- TODO rain will be snow if season+map+t° ? end if (self.Fog) then sSignificativeWeather = Fg.AppendWithSeparator(sSignificativeWeather, "FG") end if (self.Dust) then sSignificativeWeather = Fg.AppendWithSeparator(sSignificativeWeather, "DU") end sVisibility = Fg.AppendWithSeparator(sVisibility, sSignificativeWeather) local sClouds local cloudDensity, iCloudBase = self:GetFormattedClouds(true) if (cloudDensity == CloudDensityLabel.Clear) then sClouds = "SKC" elseif (cloudDensity == CloudDensityLabel.Cavok) then sClouds = "CAVOK" sVisibility = nil else local sDensity = "FEW" if (cloudDensity == CloudDensityLabel.Scattered) then sDensity = "SCT" elseif (cloudDensity == CloudDensityLabel.Broken) then sDensity = "BKN" elseif (cloudDensity == CloudDensityLabel.Overcast) then sDensity = "OVC" end sClouds = string.format("%s%03d", sDensity, iCloudBase) end local sTemperature local iTemperature = UTILS.Round(self.TemperatureCelcius) if (iTemperature >= 0) then sTemperature = string.format("%02d", iTemperature) else sTemperature = string.format("M%02d", -iTemperature) end local sQnh = string.format("Q%d/%.2f", self.QnhHpa, UTILS.hPa2inHg(self.QnhHpa)) local sMetar = Fg.TimeToStringMetar() sMetar = sMetar .. " " .. sWind sMetar = Fg.AppendWithSeparator (sMetar, sVisibility, " ") sMetar = sMetar .. " " .. sClouds sMetar = sMetar .. " " .. sTemperature sMetar = sMetar .. " " .. sQnh return sMetar end function FgWeather.CreateMetarMark(mooseCoord, mooseGroup) local weather = FgWeather:Create(mooseCoord) local sMetar = weather:ToStringMetar() local vec3 = mooseCoord:GetVec3() local iMarkId = UTILS.GetMarkID() if (mooseGroup) then trigger.action.markToGroup(iMarkId, sMetar, vec3, mooseGroup:GetDCSObject():getID(), false, nil) else trigger.action.markToAll(iMarkId, sMetar, vec3, false, nil) end return iMarkId end ]] --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- ATIS management class --- Simulation of the recording of an ATIS information per hour per airfield --- For each info a recording time and corresponding letter is generated (just to fluff it) --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veafWeatherAtis = {} veafWeatherAtis.__index = veafWeatherAtis veafWeatherAtis.ListInEffect = {} --------------------------------------------------------------------------------------------------- --- CTORS function veafWeatherAtis:Create(veafAirbase, dateTimeZulu) local iHoursSinceMidnight = dateTimeZulu.hour local sLetter = string.char(math.floor(iHoursSinceMidnight) + string.byte("A")) local iRecordedAtMinutes = math.random(2, 11) -- ATIS recorded between h:02 and hour:11 if (iRecordedAtMinutes > dateTimeZulu.min) then -- if record is in the future set recording at the request time iRecordedAtMinutes = dateTimeZulu.min end dateTimeZulu.min = iRecordedAtMinutes local iAltitude = nil local unitSystem if (veafAirbase.Category == Airbase.Category.SHIP) then -- Maybe use the type name to decide the unit system? --[[ local dcsShip = dcsAirbase:getUnit() local dcsShipType = dcsShip:getTypeName() veaf.loggers.get(veafWeather.Id):trace(veaf.p(dcsShipType)) ]] iAltitude = 20 unitSystem = veafWeatherUnitSystem.Systems.FaaNavy else unitSystem = veafWeatherUnitSystem.defaultForTheatre() end local weatherData = veafWeatherData:create(veafAirbase.DcsAirbase:getPoint(), nil, iAltitude) local sMessage if (veafAirbase.Category == Airbase.Category.SHIP) then sMessage = string.format("%s information at %sZ", veafAirbase.DisplayName, veafTime.toStringTime(dateTimeZulu, false)) local iCarrierCase = weatherData:getCarrierCase() if (iCarrierCase) then local sCaseString = nil if (iCarrierCase == 1) then sCaseString = "I" elseif (iCarrierCase == 2) then sCaseString = "II" elseif (iCarrierCase == 3) then sCaseString = "III" end if (not veaf.isNullOrEmpty(sCaseString)) then sMessage = sMessage .. string.format("\nProbable CASE %s in effect", sCaseString) end end elseif (veafAirbase.Category == Airbase.Category.HELIPAD) then sMessage = string.format("%s information at %sZ", veafAirbase.DisplayName, veafTime.toStringTime(dateTimeZulu, false)) else sMessage = string.format("%s information %s, recorded at %sZ", veafAirbase.Name, sLetter, veafTime.toStringTime(dateTimeZulu, false)) local sRunwayInService = veafAirbase:getRunwayInServiceString(weatherData.WindDirection) if (not veaf.isNullOrEmpty(sRunwayInService)) then sMessage = sMessage .. string.format("\nRecommended runway %s", sRunwayInService) end end sMessage = sMessage .. "\n" .. weatherData:toStringAtis(unitSystem) --sMessage = sMessage .. "\n" .. weatherData:toStringExtended() local this = { AirbaseName = veafAirbase.Name, Letter = sLetter, DateTimeZulu = dateTimeZulu, Message = sMessage } setmetatable(this, self) return this end --------------------------------------------------------------------------------------------------- --- Methods --------------------------------------------------------------------------------------------------- --- Static methods function veafWeatherAtis.getAtis(veafAirbase) local iAbsTime = timer.getAbsTime() local dateTime = veafTime.absTimeToDateTime(iAbsTime) local dateTimeZulu = veafTime.toZulu(dateTime) veaf.loggers.get(veafWeather.Id):trace(string.format("Preparing ATIS for airbase %s at %sZ", veafAirbase.Name, veafTime.toStringTime(dateTimeZulu, false))) local atisInEffect = veafWeatherAtis.ListInEffect[veafAirbase.Name] if (atisInEffect) then veaf.loggers.get(veafWeather.Id):trace(string.format("ATIS in effect: %s %s", atisInEffect.Letter, veafTime.toStringTime(atisInEffect.DateTimeZulu, false))) if (dateTimeZulu.year > atisInEffect.DateTimeZulu.year or dateTimeZulu.month > atisInEffect.DateTimeZulu.month or dateTimeZulu.day > atisInEffect.DateTimeZulu.day or dateTimeZulu.hour > atisInEffect.DateTimeZulu.hour) then -- if current date is in the next hour of more from the current one, declare new atis veaf.loggers.get(veafWeather.Id):trace(string.format("Current time %s: new ATIS", veafTime.toStringTime(dateTimeZulu, false))) atisInEffect = nil end end if (atisInEffect == nil) then atisInEffect = veafWeatherAtis:Create(veafAirbase, dateTimeZulu) veaf.loggers.get(veafWeather.Id):trace(string.format("New ATIS in effect for airbase %s: %s %s", veafAirbase.Name, atisInEffect.Letter, veafTime.toStringTime(atisInEffect.DateTimeZulu, false))) veafWeatherAtis.ListInEffect[veafAirbase.Name] = atisInEffect end return atisInEffect end function veafWeatherAtis.getAtisString(veafAirbase) local atisInEffect = veafWeatherAtis.getAtis(veafAirbase) return atisInEffect.Message end function veafWeatherAtis.getAtisStringFromVeafPoint(sPointName, iAbsTime) if (veaf.isNullOrEmpty(sPointName)) then veaf.loggers.get(veafWeather.Id):error("No point name") return "No point name" end local dcsAirbase = veaf.findDcsAirbase(sPointName) local veafAirbase = veafAirbases.getAirbaseFromDcsAirbase(dcsAirbase) if (veafAirbase == nil) then veaf.loggers.get(veafWeather.Id):error("Airbase not found for point " .. sPointName) return "Airbase not found for point " .. sPointName end veaf.loggers.get(veafWeather.Id):trace("Airbase found from veaf point named %s: %s",veaf.p(sPointName), veaf.p(veaf.ifnn(dcsAirbase, "getName"))) return veafWeatherAtis.getAtisString(veafAirbase, iAbsTime) end function veafWeather.messageWeatherAtClosestPoint(unitName, forUnit) veaf.loggers.get(veafWeather.Id):debug("veafWeather.messageWeatherAtClosestPoint(unitName=%s)",veaf.p(unitName)) local closestPoint = veafNamedPoints.getNearestPoint(unitName) if closestPoint then local BR = veafNamedPoints.getPointBearing({closestPoint.name, unitName}) if BR then BR = " ("..BR..")" else BR = "" end local weatherReport = "WEATHER : " .. closestPoint.name .. BR .. "\n\n" weatherReport = weatherReport .. veafWeatherData.getWeatherString(closestPoint, unitName) if forUnit then veaf.outTextForUnit(unitName, weatherReport, 30) else veaf.outTextForGroup(unitName, weatherReport, 30) end end end function veafWeather.messageAtcClosestAirbase(unitName, forUnit) local dcsUnit = Unit.getByName(unitName) local veafAirbase = veafAirbases.getNearestAirbase(dcsUnit) if (veafAirbase) then local sAtcReport = veafWeatherAtis.getAtisString(veafAirbase) if forUnit then veaf.outTextForUnit(dcsUnit:getName(), sAtcReport, 30) else veaf.outTextForGroup(dcsUnit:getName(), sAtcReport, 30) end end end function veafWeather.messageAtcAndWeather(unitName, forUnit) veafWeather.messageAtcClosestAirbase(unitName, forUnit) veafWeather.messageWeatherAtClosestPoint(unitName, forUnit) end ---------------------------------------------------------------------------------------------------- --- WEATHER modifications during runtime ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafFog class methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafFog = {} VeafFog.DELAY_BETWEEN_DYNAMIC_CHECKS = 5 * 60 VeafFog.DYNAMICFOG_BASEFACTOR_HEAVY = 0.8 VeafFog.DYNAMICFOG_BASEFACTOR_MEDIUM = 0.5 VeafFog.DYNAMICFOG_BASEFACTOR_SPARSE = 0.2 function VeafFog.init(object) -- technical name object.name = nil -- scheduled function that is used to update the object object.dynamicCheckFunctionScheduled = nil -- if true, the object is enabled and the fog settings (static and/or dynamic) are applied object.enabled = false -- the dynamic fog parameters for this object object.fogAnimationData = {} -- the static fog parameters for this object object.fogStaticData = {visibility = 10000, thickness = 150} -- the static fog parameters saved by this object (stored state before it changes them) object.savedFogStaticData = nil -- if set to a base fog factor (between 0 and 1), the fog will be dynamically computed based on latitude, season and time of day object.dynamicFogBaseFactor = nil -- if true, the dynamic fog system will animate the transitions object.dynamicFogIsAnimated = true end function VeafFog:new(objectToCopy) veaf.loggers.get(veafWeather.Id):debug("VeafFog:new()") local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object VeafFog.init(objectToCreate) return objectToCreate end function VeafFog:activate() veaf.loggers.get(veafWeather.Id):debug("VeafFog[%s]:activate()", veaf.p(self.name)) veafWeather.setAndActivateFog(self) end function VeafFog:enable() veaf.loggers.get(veafWeather.Id):debug("VeafFog[%s]:enable()", veaf.p(self.name)) self.enabled = true -- store the existing fog parameters veaf.loggers.get(veafWeather.Id):trace("store the existing fog parameters") veaf.loggers.get(veafWeather.Id):trace("world.weather.getFogVisibilityDistance()=[%s]", veaf.p(world.weather.getFogVisibilityDistance())) veaf.loggers.get(veafWeather.Id):trace("world.weather.getFogThickness()=[%s]", veaf.p(world.weather.getFogThickness())) self.fogSavedStaticData = {visibility = world.weather.getFogVisibilityDistance(), thickness = world.weather.getFogThickness()} -- set the fog to the programmed parameters veaf.loggers.get(veafWeather.Id):trace("set the fog to the programmed parameters") if self.forAnimationData then veaf.loggers.get(veafWeather.Id):trace("self.forAnimationData=[%s]", veaf.p(self.forAnimationData)) -- create an animation local animation = { self.forAnimationData } -- first reset fog animation veaf.loggers.get(veafWeather.Id):trace("first reset fog animation") world.weather.setFogAnimation({}) veaf.loggers.get(veafWeather.Id):trace("store the existing fog parameters") veaf.loggers.get(veafWeather.Id):trace("world.weather.getFogVisibilityDistance()=[%s]", veaf.p(world.weather.getFogVisibilityDistance())) veaf.loggers.get(veafWeather.Id):trace("world.weather.getFogThickness()=[%s]", veaf.p(world.weather.getFogThickness())) -- set the new fog animation world.weather.setFogAnimation(animation) elseif self.fogStaticData then veaf.loggers.get(veafWeather.Id):trace("self.fogStaticData=[%s]", veaf.p(self.fogStaticData)) world.weather.setFogThickness(self.fogStaticData.thickness) world.weather.setFogVisibilityDistance(self.fogStaticData.visibility) end -- do the first check, the method will reschedule itself veaf.loggers.get(veafWeather.Id):trace("do the first check, the method will reschedule itself") self:dynamicCheck() return self end function VeafFog:disable(dontRestore) veaf.loggers.get(veafWeather.Id):debug("VeafFog[%s]:disable()", veaf.p(self.name)) -- disable the scheduler if self.dynamicCheckFunctionScheduled then veaf.loggers.get(veafWeather.Id):trace("disable the scheduler") mist.removeFunction(self.dynamicCheckFunctionScheduled) self.dynamicCheckFunctionScheduled = nil end self.enabled = false if not dontRestore then -- reset to the fog values stored at start veaf.loggers.get(veafWeather.Id):trace("reset to the fog values stored at start") if self.fogSavedStaticData then world.weather.setFogThickness(self.fogSavedStaticData.thickness) world.weather.setFogVisibilityDistance(self.fogSavedStaticData.visibility) end end return self end function VeafFog:dynamicCheck() veaf.loggers.get(veafWeather.Id):debug("VeafFog[%s]:dynamicCheck()", veaf.p(self.name)) if self.dynamicFogBaseFactor then local position = {x=0, y=0, z=0} -- somewhere in the map ^^ -- compute the fog that should be set at this moment in time local date = veafTime.getMissionDateTime() veaf.loggers.get(veafWeather.Id):trace("date=[%s]", veaf.p(date)) -- Seasonal adjustment based on latitude and time of year local latitude, _, _ = coord.LOtoLL(position) veaf.loggers.get(veafWeather.Id):trace("latitude=[%s]", veaf.p(latitude)) local month = date.month local seasonal_peaks = { [3] = 0.8, [4] = 0.9, [5] = 0.7, [9] = 0.8, [10] = 0.9, [11] = 0.7 } local season_factor = seasonal_peaks[month] or 0.5 -- Temperature-dew point difference local weatherData = veafWeatherData:create(position) local temp_diff = math.abs(weatherData.TemperatureCelcius - weatherData.DewPointCelcius) -- Diurnal adjustment based on hour local diurnal_factor = 0 -- Convert sunrise and sunset times to minutes since midnight local sunrise_time = weatherData.SunriseLocal.hour * 60 + weatherData.SunriseLocal.min local sunset_time = weatherData.SunsetLocal.hour * 60 + weatherData.SunsetLocal.min -- Calculate daylight duration local daylight_duration = sunset_time - sunrise_time -- Morning is from sunrise and lasts 25% of daylight local morningEnd_time = sunrise_time + 0.25 * daylight_duration local morningPeak_time = sunrise_time + 0.125 * daylight_duration -- Evening start is 25% before sunset local eveningStart_time = sunset_time - 0.25 * daylight_duration local eveningPeak_time = sunset_time - 0.125 * daylight_duration -- Convert current time to minutes since midnight local current_time = date.hour * 60 + date.min veaf.loggers.get(veafWeather.Id):trace("sunrise_time=[%s:%s]", veaf.p(math.floor(sunrise_time/60)), veaf.p(math.fmod(sunrise_time,60))) veaf.loggers.get(veafWeather.Id):trace("morningPeak_time=[%s:%s]", veaf.p(math.floor(morningPeak_time/60)), veaf.p(math.fmod(morningPeak_time,60))) veaf.loggers.get(veafWeather.Id):trace("morningEnd_time=[%s:%s]", veaf.p(math.floor(morningEnd_time/60)), veaf.p(math.fmod(morningEnd_time,60))) veaf.loggers.get(veafWeather.Id):trace("eveningStart_time=[%s:%s]", veaf.p(math.floor(eveningStart_time/60)), veaf.p(math.fmod(eveningStart_time,60))) veaf.loggers.get(veafWeather.Id):trace("eveningPeak_time=[%s:%s]", veaf.p(math.floor(eveningPeak_time/60)), veaf.p(math.fmod(eveningPeak_time,60))) veaf.loggers.get(veafWeather.Id):trace("sunset_time=[%s:%s]", veaf.p(math.floor(sunset_time/60)), veaf.p(math.fmod(sunset_time,60))) veaf.loggers.get(veafWeather.Id):trace("daylight_duration=[%s]", veaf.p(daylight_duration)) veaf.loggers.get(veafWeather.Id):trace("current_time=[%s:%s]", veaf.p(math.floor(current_time/60)), veaf.p(math.fmod(current_time,60))) if current_time >= sunrise_time and current_time < morningPeak_time then -- Phase 1: From sunrise to middle of the morning (raise) veaf.loggers.get(veafWeather.Id):trace("Phase 1: From sunrise to middle of the morning (raise)") diurnal_factor = (current_time - sunrise_time) / (morningPeak_time - sunrise_time) elseif current_time >= morningPeak_time and current_time < morningEnd_time then -- Phase 2: From middle of the morning to end of the morning (decrease) veaf.loggers.get(veafWeather.Id):trace("Phase 2: From middle of the morning to end of the morning (decrease)") diurnal_factor = 1 - (current_time - morningPeak_time) / (morningEnd_time - morningPeak_time) elseif current_time >= morningEnd_time and current_time < eveningStart_time then -- Phase 3: Day phase (constant base value) veaf.loggers.get(veafWeather.Id):trace("Phase 3: Day phase (constant base value)") diurnal_factor = 0.1 elseif current_time >= eveningStart_time and current_time < eveningPeak_time then -- Phase 4: From start of the evening to middle of the evening (raise) veaf.loggers.get(veafWeather.Id):trace("Phase 4: From start of the evening to middle of the evening (raise)") diurnal_factor = (current_time - eveningStart_time) / (eveningPeak_time - eveningStart_time) elseif current_time >= eveningStart_time and current_time < sunset_time then -- Phase 5: From middle of the evening to sunset (decrease) veaf.loggers.get(veafWeather.Id):trace("Phase 5: From middle of the evening to sunset (decrease)") diurnal_factor = 1 - (current_time - eveningPeak_time) / (sunset_time - eveningPeak_time) end -- Base fog probability calculation local base_prob = math.max(0, math.min(1, 1 - (temp_diff / 10) - (weatherData.WindSpeedMps / 10))) local fog_probability = base_prob * season_factor * diurnal_factor veaf.loggers.get(veafWeather.Id):trace("weatherData.WindSpeedMps=[%s]", veaf.p(weatherData.WindSpeedMps)) veaf.loggers.get(veafWeather.Id):trace("temp_diff=[%s]", veaf.p(temp_diff)) veaf.loggers.get(veafWeather.Id):trace("base_prob=[%s]", veaf.p(base_prob)) veaf.loggers.get(veafWeather.Id):trace("season_factor=[%s]", veaf.p(season_factor)) veaf.loggers.get(veafWeather.Id):trace("diurnal_factor=[%s]", veaf.p(diurnal_factor)) veaf.loggers.get(veafWeather.Id):trace("fog_probability=[%s]", veaf.p(fog_probability)) -- Compute visibility and thickness based on fog_probability with smooth transitions local visibility, thickness if fog_probability < 0.2 then visibility = 50000 * (1 - fog_probability) -- High visibility as fog_probability decreases thickness = 0 -- No fog, so no thickness else -- Normalize the fog factor relative to the 0.2-1.0 range local normalizedFactor = (fog_probability - 0.2) / 0.8 local minVisibility = 100 * (1 - self.dynamicFogBaseFactor) local maxVisibility = 5000 * (1 - self.dynamicFogBaseFactor) local minThickness = 100 * self.dynamicFogBaseFactor local maxThickness = 500 * self.dynamicFogBaseFactor veaf.loggers.get(veafWeather.Id):trace("minVisibility=[%s]", veaf.p(minVisibility)) veaf.loggers.get(veafWeather.Id):trace("maxVisibility=[%s]", veaf.p(maxVisibility)) veaf.loggers.get(veafWeather.Id):trace("minThickness=[%s]", veaf.p(minThickness)) veaf.loggers.get(veafWeather.Id):trace("maxThickness=[%s]", veaf.p(maxThickness)) -- Calculate visibility (decreasing from 5000 to 100) visibility = maxVisibility - ((maxVisibility - minVisibility) * normalizedFactor) -- Calculate thickness (increasing from 100 to 1000) thickness = minThickness + ((maxThickness - minThickness) * normalizedFactor) end visibility = math.floor(visibility) thickness = math.floor(thickness) veaf.loggers.get(veafWeather.Id):trace("thickness=[%s]", veaf.p(thickness)) veaf.loggers.get(veafWeather.Id):trace("visibility=[%s]", veaf.p(visibility)) if self.dynamicFogIsAnimated then -- create an animation veaf.loggers.get(veafWeather.Id):trace("thickness=[%s]", veaf.p(thickness)) local animation = { VeafFog.DELAY_BETWEEN_DYNAMIC_CHECKS - VeafFog.DELAY_BETWEEN_DYNAMIC_CHECKS*0.1, visibility, thickness } veaf.loggers.get(veafWeather.Id):trace("animation=[%s]", veaf.p(animation)) -- first reset fog animation world.weather.setFogAnimation({}) -- set the new fog animation world.weather.setFogAnimation(animation) else world.weather.setFogThickness(thickness) world.weather.setFogVisibilityDistance(visibility) end end -- reschedule self.dynamicCheckFunctionScheduled = mist.scheduleFunction(VeafFog.dynamicCheck, {self}, timer.getTime() + VeafFog.DELAY_BETWEEN_DYNAMIC_CHECKS) end function veafWeather.createStaticFog(name, thickness, visibility) local fog = VeafFog:new() fog.name = name fog.fogStaticData = { thickness = thickness, visibility = visibility} return fog end function veafWeather.createDynamicFog(name, baseFactor, notAnimated) local fog = VeafFog:new() fog.name = name fog.dynamicFogBaseFactor = baseFactor fog.dynamicFogIsAnimated = not notAnimated return fog end function veafWeather.createAnimatedFog(name, minutes, thickness, visibility) local fog = VeafFog:new() fog.name = name fog.forAnimationData = {minutes * 60, visibility, thickness} return fog end function veafWeather.setAndActivateFog(fogObject) veaf.loggers.get(veafWeather.Id):trace("fogObject=[%s]", veaf.p(fogObject)) -- disable the existing fog object if any if veafWeather.existingFog ~= nil then veaf.loggers.get(veafWeather.Id):trace("disable the existing fog object if any") veaf.loggers.get(veafWeather.Id):trace("veafWeather.existingFog=[%s]", veaf.p(veafWeather.existingFog)) veafWeather.existingFog:disable(true) end -- activate the new fog object veaf.loggers.get(veafWeather.Id):trace("activate the new fog object") veafWeather.existingFog = fogObject fogObject:enable() trigger.action.outText("Fog set to "..fogObject.name, 5) return fogObject end -- dynamically managed fog instances veafWeather.FOG_DYNAMIC_HEAVY = veafWeather.createDynamicFog("Dynamic HEAVY fog", VeafFog.DYNAMICFOG_BASEFACTOR_HEAVY) veafWeather.FOG_DYNAMIC_MEDIUM = veafWeather.createDynamicFog("Dynamic MEDIUM fog", VeafFog.DYNAMICFOG_BASEFACTOR_MEDIUM) veafWeather.FOG_DYNAMIC_SPARSE = veafWeather.createDynamicFog("Dynamic SPARSE fog", VeafFog.DYNAMICFOG_BASEFACTOR_SPARSE) -- static fog instances veafWeather.FOG_STATIC_HEAVY = veafWeather.createStaticFog("Static HEAVY fog", 500, 100) veafWeather.FOG_STATIC_MEDIUM = veafWeather.createStaticFog("Static MEDIUM fog", 500, 500) veafWeather.FOG_STATIC_MEDIUM_LOW = veafWeather.createStaticFog("Static MEDIUM LOW fog", 100, 500) veafWeather.FOG_STATIC_SPARSE = veafWeather.createStaticFog("Static SPARSE fog", 500, 5000) veafWeather.FOG_STATIC_SPARSE_LOW = veafWeather.createStaticFog("Static SPARSE LOW fog", 100, 5000) veafWeather.FOG_STATIC_NO = veafWeather.createStaticFog("Static NO fog", 0, 0) -- animated fog instances for _, minutes in pairs({1, 5, 10, 15, 30, 60, 90}) do local overMinutesText = string.format(" over %d minutes", minutes) veafWeather["FOG_ANIMATED_"..minutes.."M_HEAVY"] = veafWeather.createAnimatedFog("Animated HEAVY fog"..overMinutesText, minutes, 500, 100) veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM"] = veafWeather.createAnimatedFog("Animated MEDIUM fog"..overMinutesText, minutes, 500, 500) veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM_LOW"] = veafWeather.createAnimatedFog("Animated MEDIUM LOW fog"..overMinutesText, minutes, 100, 500) veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE"] = veafWeather.createAnimatedFog("Animated SPARSE fog"..overMinutesText, minutes, 500, 5000) veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE_LOW"] = veafWeather.createAnimatedFog("Animated SPARSE LOW fog"..overMinutesText, minutes, 100, 5000) veafWeather["FOG_ANIMATED_"..minutes.."M_NO"] = veafWeather.createAnimatedFog("Animated NO fog"..overMinutesText, minutes, 0, 0) end --------------------------------------------------------------------------------------------------- --- Radio menu and remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Build the initial radio menu function veafWeather.buildRadioMenu() veaf.loggers.get(veafWeather.Id):debug("buildRadioMenu()") veafWeather.rootPath = veafRadio.addMenu(veafWeather.RadioMenuName) veafRadio.addCommandToSubmenu("Weather on closest point" , veafWeather.rootPath, veafWeather.messageWeatherAtClosestPoint, nil, veafRadio.USAGE_ForGroup) veafRadio.addCommandToSubmenu("ATC on closest airbase" , veafWeather.rootPath, veafWeather.messageAtcClosestAirbase, nil, veafRadio.USAGE_ForGroup) veafRadio.addCommandToSubmenu("ATC and weather in one go" , veafWeather.rootPath, veafWeather.messageAtcAndWeather, nil, veafRadio.USAGE_ForGroup) local fogPath = veafRadio.addSubMenu("Fog settings", veafWeather.rootPath) local dynamicFogPath = veafRadio.addSubMenu("Dynamic fog", fogPath) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_DYNAMIC_HEAVY.name, dynamicFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_DYNAMIC_HEAVY, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_DYNAMIC_MEDIUM.name, dynamicFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_DYNAMIC_MEDIUM, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_DYNAMIC_SPARSE.name, dynamicFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_DYNAMIC_SPARSE, veafRadio.USAGE_ForAll) local animatedFogPath = veafRadio.addSubMenu("Animated fog", fogPath) for _, minutes in pairs({1, 5, 10, 15, 30, 60, 90}) do local overMinutesText = string.format(" over %d minutes", minutes) local _path = veafRadio.addSubMenu("Animated fog"..overMinutesText, animatedFogPath) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_HEAVY"].name, _path, veafWeather.setAndActivateFog, veafWeather["FOG_ANIMATED_"..minutes.."M_HEAVY"], veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM"].name, _path, veafWeather.setAndActivateFog, veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM"], veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM_LOW"].name, _path, veafWeather.setAndActivateFog, veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM_LOW"], veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE"].name, _path, veafWeather.setAndActivateFog, veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE"], veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE_LOW"].name, _path, veafWeather.setAndActivateFog, veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE_LOW"], veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_NO"].name, _path, veafWeather.setAndActivateFog, veafWeather.FOG_ANIMATED_5_NO, veafRadio.USAGE_ForAll) end local staticFogPath = veafRadio.addSubMenu("Static fog", fogPath) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_HEAVY.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_HEAVY, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_MEDIUM.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_MEDIUM, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_MEDIUM_LOW.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_MEDIUM_LOW, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_SPARSE.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_SPARSE, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_SPARSE_LOW.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_SPARSE_LOW, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_NO.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_NO, veafRadio.USAGE_ForAll) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- execute command from the remote interface function veafWeather.executeCommandFromRemote(parameters) veaf.loggers.get(veafWeather.Id):debug(string.format("veafWeather.executeCommandFromRemote()")) veaf.loggers.get(veafWeather.Id):trace(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) veaf.loggers.get(veafWeather.Id):trace(string.format("_pilot= %s", veaf.p(_pilot))) veaf.loggers.get(veafWeather.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName))) veaf.loggers.get(veafWeather.Id):trace(string.format("_unitName= %s", veaf.p(_unitName))) veaf.loggers.get(veafWeather.Id):trace(string.format("_command= %s", veaf.p(_command))) if not _pilot or not _command then return false end if _command then -- parse the command local _action, _name, _parameters = _command:match(veafWeather.RemoteCommandParser) veaf.loggers.get(veafWeather.Id):trace(string.format("_action=%s",veaf.p(_action))) veaf.loggers.get(veafWeather.Id):trace(string.format("_name=%s",veaf.p(_name))) veaf.loggers.get(veafWeather.Id):trace(string.format("_parameters=%s",veaf.p(_parameters))) if _action and _action:lower() == "weather" then veaf.loggers.get(veafWeather.Id):info(string.format("[%s] is requesting weather",veaf.p(_pilotName))) veafWeather.messageWeatherAtClosestPoint(_unitName, true) return true elseif _action and _action:lower() == "atc" then veaf.loggers.get(veafWeather.Id):info(string.format("[%s] is requesting atc",veaf.p(_pilotName))) veafWeather.messageAtcClosestAirbase(_unitName, true) return true elseif not _action or _action:lower() == "all" then veaf.loggers.get(veafWeather.Id):info(string.format("[%s] is requesting both atc and weather",veaf.p(_pilotName))) veafWeather.messageAtcAndWeather(_unitName, true) return true elseif _action and _action:lower() == "fog" then if _name then local uName = _name:upper() local fogObject = veafWeather[uName] if fogObject then veaf.loggers.get(veafWeather.Id):info(string.format("[%s] is requesting fog [%s]",veaf.p(_pilotName), veaf.p(uName))) veafWeather.setAndActivateFog(fogObject) return true end end end end return false end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- MAIN MODULE INITIALIZATION --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- function veafWeather.initialize() veaf.loggers.get(veafWeather.Id):debug("veafWeather.initialize()") veafWeather.buildRadioMenu() -- TODO veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafWeather.onEventMarkChange) veafAirbases.initialize() end veaf.loggers.get(veafWeather.Id):info(string.format("Loading version %s", veafWeather.Version)) --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- -------------------- TEST STUFF-------------------------------------------------------------------- --[[ .getFogThickness=[function] .getFogVisibilityDistance=[function] .setFogAnimation=[function] .setFogThickness=[function] .setFogVisibilityDistance=[function] veafAirbases.initialize() for _, veafAirbase in pairs(veafAirbases.Airbases) do veaf.loggers.get(veafWeather.Id):trace(veafWeatherAtis.getAtisString(veafAirbase)) veaf.loggers.get(veafWeather.Id):trace(veafWeatherData.getWeatherString(veafAirbase.DcsAirbase:getPoint())) end veaf.loggers.get(veafWeather.Id):trace(veaf.p(env.mission.weather.enable_fog)) veaf.loggers.get(veafWeather.Id):trace(veaf.p(world.weather.getFogVisibilityDistance())) veaf.loggers.get(veafWeather.Id):trace(veaf.p(world.weather.getFogThickness())) ]] ------------------ END script veafWeather.lua ------------------ ----------------------------------------------------------------------------------- -- END OF Veaf scripts 5.103.3/debug;2025.12.16.12.16.24 -----------------------------------------------------------------------------------