----------------------------------------------------------------------------------- -- Veaf scripts 5.81.0;2025.03.29.14.39.59 ----------------------------------------------------------------------------------- ------------------ 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.54.0" --- Development version ? veaf.Development = false veaf.SecurityDisabled = false -- trace level, specific to this module --veaf.LogLevel = "debug" --veaf.LogLevel = "trace" --veaf.ForcedLogLevel = "trace" -- log level, limiting all the modules veaf.BaseLogLevel = 5 --trace veaf.DEFAULT_GROUND_SPEED_KPH = 30 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- 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", } ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- 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) -- LOGGING DISABLED WHEN COMPILING(string.format("getLandHeight: vec3 x=%.1f y=%.1f, z=%.1f", vec3.x, vec3.y, vec3.z)) local vec2 = {x = vec3.x, y = vec3.z} -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("getLandHeight: result height=%.1f",height)) return height end function veaf.invertHeading(heading) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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+)") -- LOGGING DISABLED WHEN COMPILING(string.format("_zone=%s",veaf.p(_zone))) -- LOGGING DISABLED WHEN COMPILING(string.format("_digraph=%s",veaf.p(_digraph))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 } -- LOGGING DISABLED WHEN COMPILING(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:\.-]+)]]) 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 -- 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 -- LOGGING DISABLED WHEN COMPILING(string.format("getLandHeight: vec3 x=%.1f y=%.1f, z=%.1f", vec3.x, vec3.y, vec3.z)) local height = veaf.getLandHeight(vec3) -- LOGGING DISABLED WHEN COMPILING(string.format("getLandHeight: result height=%.1f",height)) local result={x=vec3.x, y=height, z=vec3.z} -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(string.format("Wind data: point x=%.1f y=%.1f, z=%.1f", point.x, point.y,point.z)) -- LOGGING DISABLED WHEN COMPILING(string.format("Wind data: wind x=%.1f y=%.1f, z=%.1f", windvec3.x, windvec3.y,windvec3.z)) -- LOGGING DISABLED WHEN COMPILING(string.format("Wind data: |v| = %.1f", strength)) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("coarse mach2 = %s", veaf.p(mach2)) mach2 = converge_2_DPP(0.0125) - 0.0125 --medium -- LOGGING DISABLED WHEN COMPILING("medium mach2 = %s", veaf.p(mach2)) mach2 = converge_2_DPP(0.00625) --fine -- LOGGING DISABLED WHEN COMPILING("fine mach2 = %s", veaf.p(mach2)) else mach2 = math.sqrt(2*((DPP1+1)^(1/B)-1)/(Gamma-1)) -- LOGGING DISABLED WHEN COMPILING("subsonic mach2 = %s", veaf.p(mach2)) end return mach2 end local ms_2_kt = 1.94384 local a1 = speedOfSound(temperature) local a0 = speedOfSound(T0) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("_lat=%s",veaf.p(_lat))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("endPoint=%s", veaf.p(endPoint))) local road_x = nil local road_z = nil local trueStartPoint = mist.utils.deepCopy(startPoint) if onRoad then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(string.format("startPoint = {x = %d, y = %d, z = %d}", startPoint.x, startPoint.y, startPoint.z)) local trueEndPoint = mist.utils.deepCopy(endPoint) if onRoad then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("vehiclesRoute = %s", veaf.p(vehiclesRoute))) return vehiclesRoute end function veaf.PatrolWatchdog(groupName,patrolRoute,speed,firstPass) -- LOGGING DISABLED WHEN COMPILING(string.format("veaf.PatrolWatchdog(groupName=%s, speed=%s, firstPass=%s)", veaf.p(groupName), veaf.p(speed), veaf.p(firstPass))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Lead vehicule name : %s", veaf.p(groupUnits[1]:getName()))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("Lead vehicle is passing in the bubble, rescheduling in " .. rescheduleTime .. "s !") else -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Lead vehicle not active, rescheduling in 60s !") mist.scheduleFunction(veaf.PatrolWatchdog,{groupName, patrolRoute, speed, firstPass}, timer.getTime()+60) end end end -- LOGGING DISABLED WHEN COMPILING("========================================================================") 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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("headingRad="..headingRad) local fromPosition = leadUnit:getPosition().p fromPosition = { x = fromPosition.x, y = fromPosition.z } -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("length="..length .. " m") -- new route point local newWaypoint2 = { x = fromPosition.x + length * math.cos(headingRad), y = fromPosition.y + length * math.sin(headingRad), } -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("veaf.moveGroupTo(groupName=" .. groupName .. ", speed=".. speed .. ", altitude=".. altitude) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("startingPoint="..veaf.vecToString(startingPoint)) -- LOGGING DISABLED WHEN COMPILING("destinationPoint="..veaf.vecToString(destinationPoint)) local vecAB = {x = destinationPoint.x +- startingPoint.x, y = destinationPoint.y - startingPoint.y, z = destinationPoint.z - startingPoint.z} -- LOGGING DISABLED WHEN COMPILING("vecAB="..veaf.vecToString(vecAB)) local alpha = math.atan2(vecAB.x, vecAB.z) -- atan2(y, x) -- LOGGING DISABLED WHEN COMPILING("alpha="..alpha) local r = math.sqrt(distanceFromStartingPoint * distanceFromStartingPoint + offset * offset) -- LOGGING DISABLED WHEN COMPILING("r="..r) local beta = math.atan(offset / distanceFromStartingPoint) -- LOGGING DISABLED WHEN COMPILING("beta="..beta) local tho = alpha + beta -- LOGGING DISABLED WHEN COMPILING("tho="..tho) local offsetPoint = { z = r * math.cos(tho) + startingPoint.z, y = 0, x = r * math.sin(tho) + startingPoint.x} -- LOGGING DISABLED WHEN COMPILING("offsetPoint="..veaf.vecToString(offsetPoint)) local offsetPointOnLand = veaf.placePointOnLand(offsetPoint) -- LOGGING DISABLED WHEN COMPILING("offsetPointOnLand="..veaf.vecToString(offsetPointOnLand)) return offsetPointOnLand, offsetPoint end function veaf.getBearingAndRangeFromTo(fromPoint, toPoint) -- LOGGING DISABLED WHEN COMPILING("fromPoint="..veaf.vecToString(fromPoint)) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("findUnitsInCircle(radius=%s)", tostring(radius))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(".findInTable found ".. key) end return result end function veaf.getTankerData(tankerGroupName) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("found a " .. #points .. "-points route for tanker " .. tankerGroupName) for i, point in pairs(points) do -- LOGGING DISABLED WHEN COMPILING("found point #" .. i) local task = veaf.findInTable(point, "task") if task then local tasks = task.params.tasks if (tasks) then -- LOGGING DISABLED WHEN COMPILING("found " .. #tasks .. " tasks") for j, task in pairs(tasks) do -- LOGGING DISABLED WHEN COMPILING("found task #" .. j) if task.params then -- LOGGING DISABLED WHEN COMPILING("has .params") if task.params.action then -- LOGGING DISABLED WHEN COMPILING("has .action") if task.params.action.params then -- LOGGING DISABLED WHEN COMPILING("has .params") if task.params.action.params.channel then -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("found " .. #tasks .. " programmed tasks for carrier " .. carrierUnitName .. " in group " .. carrierGroupName) for i, task in pairs(tasks) do if task then -- LOGGING DISABLED WHEN COMPILING("found task #" .. i) if task.params then -- LOGGING DISABLED WHEN COMPILING("has .params") if task.params.action then local action = task.params.action -- LOGGING DISABLED WHEN COMPILING("has .action") if task.params.action.params then local actionParams = task.params.action.params -- LOGGING DISABLED WHEN COMPILING("action has .params") if task.params.action.params.unitId and task.params.action.params.unitId == carrierUnitId then -- LOGGING DISABLED WHEN COMPILING("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}) -- LOGGING DISABLED WHEN COMPILING(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}) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("coalitionName=%s", veaf.p(coalitionName)) if not veaf.countriesByCoalition[coalitionName] then veaf.countriesByCoalition[coalitionName]={} end -- LOGGING DISABLED WHEN COMPILING("countries=%s", veaf.p(countries)) for countryName, country in pairs(countries) do -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("veaf.countriesByCoalition=%s", veaf.p(veaf.countriesByCoalition)) -- LOGGING DISABLED WHEN COMPILING("veaf.coalitionByCountry=%s", veaf.p(veaf.coalitionByCountry)) -- LOGGING DISABLED WHEN COMPILING("veaf.countriesByName=%s", veaf.p(veaf.countriesByName)) -- LOGGING DISABLED WHEN COMPILING("veaf.countriesNamesById=%s", veaf.p(veaf.countriesNamesById)) end function veaf.getCountryId(countryName) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("Associed Airbase ID : %s", veaf.p(temp))) if temp then -- LOGGING DISABLED WHEN COMPILING(string.format("Associed Airbase Coalition : %s", veaf.p(temp:getCoalition()))) if temp:getCoalition() == coa then -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("Loading Life0 of airbases...")) for _,airbase in pairs(airbases) do local airbase_name = airbase:getName() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("Airbase ID : %s", veaf.p(airbase))) if airbase then local airbase_desc = airbase:getDesc() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("Building Life : %s", veaf.p(airbase_life))) else airbase_life0 = -1 airbase_life = -1 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Airbase category does not have a default life0 setting, using life instead")) else airbase_life = -1 airbase_life0 = -1 -- LOGGING DISABLED WHEN COMPILING(string.format("Airbase category does not have a default life0 setting nor does it have a life, discarding")) end end -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Storing Life0 = Life for airbase...")) elseif not veaf.AIRBASES_LIFE0[airbase_name] then veaf.AIRBASES_LIFE0[airbase_name] = airbase_life0 -- LOGGING DISABLED WHEN COMPILING(string.format("Storing default Life0 for airbase type...")) end end if percentage then airbase_life = airbase_life_percentage end end -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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))) -- LOGGING DISABLED WHEN COMPILING(string.format("timer.getAbsTime()=%d", timer.getAbsTime())) if timer.getAbsTime() >= endTimeInSeconds then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("endTimeInSeconds=%d", endTimeInSeconds)) veaf._checkForEndMission(endTimeInSeconds, checkIntervalInSeconds, checkMessage, delay1, message1, delay2, message2, delay3, message3) end function veaf.randomlyChooseFrom(aTable, bias) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("getRandomizableNumeric_random(%s)", tostring(val))) local MIN = 0 local MAX = 99 local nVal = tonumber(val) -- LOGGING DISABLED WHEN COMPILING("nVal=%s", veaf.p(nVal)) if nVal == nil then local dashPos = string.find(val,"%-") -- LOGGING DISABLED WHEN COMPILING("dashPos=%s", veaf.p(dashPos)) if dashPos then local lower = val:sub(1, dashPos-1) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("upper=%s", veaf.p(upper)) if upper then upper = tonumber(upper) end if upper == nil then upper = MAX end nVal = math.random(lower, upper) -- LOGGING DISABLED WHEN COMPILING("nVal=%s", veaf.p(nVal)) end end --[[ if val == "0-1" then nVal = math.random(0,1) end if val == "0-2" then nVal = math.random(0,2) end if val == "0-3" then nVal = math.random(0,3) end if val == "0-4" then nVal = math.random(0,4) end if val == "0-5" then nVal = math.random(0,5) end if val == "0-6" then nVal = math.random(0,6) end if val == "0-7" then nVal = math.random(0,7) end if val == "0-8" then nVal = math.random(0,8) end if val == "0-9" then nVal = math.random(0,9) end if val == "0-10" then nVal = math.random(0,10) end if val == "0-11" then nVal = math.random(0,11) end if val == "0-12" then nVal = math.random(0,12) end if val == "0-13" then nVal = math.random(0,13) end if val == "0-14" then nVal = math.random(0,14) end if val == "0-15" then nVal = math.random(0,15) end if val == "0-16" then nVal = math.random(0,16) end if val == "0-17" then nVal = math.random(0,17) end if val == "0-18" then nVal = math.random(0,18) end if val == "0-19" then nVal = math.random(0,19) end if val == "1-2" then nVal = math.random(1,2) end if val == "1-3" then nVal = math.random(1,3) end if val == "1-4" then nVal = math.random(1,4) end if val == "1-5" then nVal = math.random(1,5) end if val == "1-6" then nVal = math.random(1,6) end if val == "1-7" then nVal = math.random(1,7) end if val == "1-8" then nVal = math.random(1,8) end if val == "1-9" then nVal = math.random(1,9) end if val == "1-10" then nVal = math.random(1,10) end if val == "1-11" then nVal = math.random(1,11) end if val == "1-12" then nVal = math.random(1,12) end if val == "1-13" then nVal = math.random(1,13) end if val == "1-14" then nVal = math.random(1,14) end if val == "1-15" then nVal = math.random(1,15) end if val == "1-16" then nVal = math.random(1,16) end if val == "1-17" then nVal = math.random(1,17) end if val == "1-18" then nVal = math.random(1,18) end if val == "1-19" then nVal = math.random(1,19) end if val == "2-3" then nVal = math.random(2,3) end if val == "2-4" then nVal = math.random(2,4) end if val == "2-5" then nVal = math.random(2,5) end if val == "2-6" then nVal = math.random(2,6) end if val == "2-7" then nVal = math.random(2,7) end if val == "2-8" then nVal = math.random(2,8) end if val == "2-9" then nVal = math.random(2,9) end if val == "2-10" then nVal = math.random(2,10) end if val == "2-11" then nVal = math.random(2,11) end if val == "2-12" then nVal = math.random(2,12) end if val == "2-13" then nVal = math.random(2,13) end if val == "2-14" then nVal = math.random(2,14) end if val == "2-15" then nVal = math.random(2,15) end if val == "2-16" then nVal = math.random(2,16) end if val == "2-17" then nVal = math.random(2,17) end if val == "2-18" then nVal = math.random(2,18) end if val == "2-19" then nVal = math.random(2,19) end if val == "3-4" then nVal = math.random(3,4) end if val == "3-5" then nVal = math.random(3,5) end if val == "3-6" then nVal = math.random(3,6) end if val == "3-7" then nVal = math.random(3,7) end if val == "3-8" then nVal = math.random(3,8) end if val == "3-9" then nVal = math.random(3,9) end if val == "3-10" then nVal = math.random(3,10) end if val == "3-11" then nVal = math.random(3,11) end if val == "3-12" then nVal = math.random(3,12) end if val == "3-13" then nVal = math.random(3,13) end if val == "3-14" then nVal = math.random(3,14) end if val == "3-15" then nVal = math.random(3,15) end if val == "3-16" then nVal = math.random(3,16) end if val == "3-17" then nVal = math.random(3,17) end if val == "3-18" then nVal = math.random(3,18) end if val == "3-19" then nVal = math.random(3,19) end if val == "4-5" then nVal = math.random(4,5) end if val == "4-6" then nVal = math.random(4,6) end if val == "4-7" then nVal = math.random(4,7) end if val == "4-8" then nVal = math.random(4,8) end if val == "4-9" then nVal = math.random(4,9) end if val == "4-10" then nVal = math.random(4,10) end if val == "4-11" then nVal = math.random(4,11) end if val == "4-12" then nVal = math.random(4,12) end if val == "4-13" then nVal = math.random(4,13) end if val == "4-14" then nVal = math.random(4,14) end if val == "4-15" then nVal = math.random(4,15) end if val == "4-16" then nVal = math.random(4,16) end if val == "4-17" then nVal = math.random(4,17) end if val == "4-18" then nVal = math.random(4,18) end if val == "4-19" then nVal = math.random(4,19) end if val == "5-6" then nVal = math.random(5,6) end if val == "5-7" then nVal = math.random(5,7) end if val == "5-8" then nVal = math.random(5,8) end if val == "5-9" then nVal = math.random(5,9) end if val == "5-10" then nVal = math.random(5,10) end if val == "5-11" then nVal = math.random(5,11) end if val == "5-12" then nVal = math.random(5,12) end if val == "5-13" then nVal = math.random(5,13) end if val == "5-14" then nVal = math.random(5,14) end if val == "5-15" then nVal = math.random(5,15) end if val == "5-16" then nVal = math.random(5,16) end if val == "5-17" then nVal = math.random(5,17) end if val == "5-18" then nVal = math.random(5,18) end if val == "5-19" then nVal = math.random(5,19) end if val == "6-7" then nVal = math.random(6,7) end if val == "6-8" then nVal = math.random(6,8) end if val == "6-9" then nVal = math.random(6,9) end if val == "6-10" then nVal = math.random(6,10) end if val == "6-11" then nVal = math.random(6,11) end if val == "6-12" then nVal = math.random(6,12) end if val == "6-13" then nVal = math.random(6,13) end if val == "6-14" then nVal = math.random(6,14) end if val == "6-15" then nVal = math.random(6,15) end if val == "6-16" then nVal = math.random(6,16) end if val == "6-17" then nVal = math.random(6,17) end if val == "6-18" then nVal = math.random(6,18) end if val == "6-19" then nVal = math.random(6,19) end if val == "7-8" then nVal = math.random(7,8) end if val == "7-9" then nVal = math.random(7,9) end if val == "7-10" then nVal = math.random(7,10) end if val == "7-11" then nVal = math.random(7,11) end if val == "7-12" then nVal = math.random(7,12) end if val == "7-13" then nVal = math.random(7,13) end if val == "7-14" then nVal = math.random(7,14) end if val == "7-15" then nVal = math.random(7,15) end if val == "7-16" then nVal = math.random(7,16) end if val == "7-17" then nVal = math.random(7,17) end if val == "7-18" then nVal = math.random(7,18) end if val == "7-19" then nVal = math.random(7,19) end if val == "8-9" then nVal = math.random(8,9) end if val == "8-10" then nVal = math.random(8,10) end if val == "8-11" then nVal = math.random(8,11) end if val == "8-12" then nVal = math.random(8,12) end if val == "8-13" then nVal = math.random(8,13) end if val == "8-14" then nVal = math.random(8,14) end if val == "8-15" then nVal = math.random(8,15) end if val == "8-16" then nVal = math.random(8,16) end if val == "8-17" then nVal = math.random(8,17) end if val == "8-18" then nVal = math.random(8,18) end if val == "8-19" then nVal = math.random(8,19) end if val == "9-10" then nVal = math.random(9,10) end if val == "9-11" then nVal = math.random(9,11) end if val == "9-12" then nVal = math.random(9,12) end if val == "9-13" then nVal = math.random(9,13) end if val == "9-14" then nVal = math.random(9,14) end if val == "9-15" then nVal = math.random(9,15) end if val == "9-16" then nVal = math.random(9,16) end if val == "9-17" then nVal = math.random(9,17) end if val == "9-18" then nVal = math.random(9,18) end if val == "9-19" then nVal = math.random(9,19) end if val == "10-11" then nVal = math.random(10,11) end if val == "10-12" then nVal = math.random(10,12) end if val == "10-13" then nVal = math.random(10,13) end if val == "10-14" then nVal = math.random(10,14) end if val == "10-15" then nVal = math.random(10,15) end if val == "10-16" then nVal = math.random(10,16) end if val == "10-17" then nVal = math.random(10,17) end if val == "10-18" then nVal = math.random(10,18) end if val == "10-19" then nVal = math.random(10,19) end if val == "11-12" then nVal = math.random(11,12) end if val == "11-13" then nVal = math.random(11,13) end if val == "11-14" then nVal = math.random(11,14) end if val == "11-15" then nVal = math.random(11,15) end if val == "11-16" then nVal = math.random(11,16) end if val == "11-17" then nVal = math.random(11,17) end if val == "11-18" then nVal = math.random(11,18) end if val == "11-19" then nVal = math.random(11,19) end if val == "12-13" then nVal = math.random(12,13) end if val == "12-14" then nVal = math.random(12,14) end if val == "12-15" then nVal = math.random(12,15) end if val == "12-16" then nVal = math.random(12,16) end if val == "12-17" then nVal = math.random(12,17) end if val == "12-18" then nVal = math.random(12,18) end if val == "12-19" then nVal = math.random(12,19) end if val == "13-14" then nVal = math.random(13,14) end if val == "13-15" then nVal = math.random(13,15) end if val == "13-16" then nVal = math.random(13,16) end if val == "13-17" then nVal = math.random(13,17) end if val == "13-18" then nVal = math.random(13,18) end if val == "13-19" then nVal = math.random(13,19) end if val == "14-15" then nVal = math.random(14,15) end if val == "14-16" then nVal = math.random(14,16) end if val == "14-17" then nVal = math.random(14,17) end if val == "14-18" then nVal = math.random(14,18) end if val == "14-19" then nVal = math.random(14,19) end if val == "15-16" then nVal = math.random(15,16) end if val == "15-17" then nVal = math.random(15,17) end if val == "15-18" then nVal = math.random(15,18) end if val == "15-19" then nVal = math.random(15,19) end if val == "16-17" then nVal = math.random(16,17) end if val == "16-18" then nVal = math.random(16,18) end if val == "16-19" then nVal = math.random(16,19) end if val == "17-18" then nVal = math.random(17,18) end if val == "17-19" then nVal = math.random(17,19) end if val == "18-19" then nVal = math.random(18,19) end ]] -- LOGGING DISABLED WHEN COMPILING(string.format("nVal=%s", tostring(nVal))) return nVal end function veaf.getRandomizableNumeric_norandom(val) -- LOGGING DISABLED WHEN COMPILING(string.format("getRandomizableNumeric_norandom(%s)", tostring(val))) local nVal = tonumber(val) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("nVal=%s", tostring(nVal))) return nVal end function veaf.getRandomizableNumeric(val) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("filepath=%s", veaf.p(l_filepath))) end if not l_filepath and l_lfs then l_filepath = l_lfs.writedir() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("filepath=%s", veaf.p(l_filepath))) end if l_filepath == "SERVER_SAVEDGAMES_DIR" then l_filepath = l_lfs.writedir() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("filename=%s", veaf.p(l_filename))) local file = l_io.open(l_filename, "a") if file then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("filepath=%s", veaf.p(l_export_path))) end if l_export_path == "SERVER_SAVEDGAMES_DIR" then l_export_path = l_lfs.writedir() -- LOGGING DISABLED WHEN COMPILING(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" -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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.getPolygonFromUnits(unitNames) -- LOGGING DISABLED WHEN COMPILING(string.format("veaf.getPolygonFromUnits()")) -- LOGGING DISABLED WHEN COMPILING(string.format("unitNames = %s", veaf.p(unitNames))) local polygon = {} for _, unitName in pairs(unitNames) do -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("position = %s", veaf.p(position))) table.insert(polygon, mist.utils.deepCopy(position)) end end -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("laser code : %s", veaf.p(code))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("Unit.getByName(dcsElementName)=%s", veaf.p(dcsUnit)) if not dcsUnit then -- then check for a group named like that local dcsGroup = Group.getByName(dcsElementName) -- LOGGING DISABLED WHEN COMPILING("Group.getByName(dcsElementName)=%s", veaf.p(dcsGroup)) dcsUnit = dcsGroup and dcsGroup:getUnit(1) end if (dcsUnit) then -- LOGGING DISABLED WHEN COMPILING("dcsUnit=%s", veaf.p(dcsUnit, nil, nil, true, false)) result = dcsUnit:getTypeName() end end -- LOGGING DISABLED WHEN COMPILING("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 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) 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 logFunction(self.name .. '|' .. levelChar .. '|' .. texts[i]) else logFunction(texts[i]) 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) 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 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- 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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("VeafDrawingOnMap[]:setName(%s)", veaf.p(value)) self.name = value return self end function VeafDrawingOnMap:getName() return self.name end function VeafDrawingOnMap:setCoalition(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("VeafDrawingOnMap[%s]:setPointsFromUnits()", veaf.p(self.name)) local polygon = veaf.getPolygonFromUnits(unitNames) self:addPoints(polygon) return self end function VeafDrawingOnMap:setColor(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("VeafDrawingOnMap[%s]:setArrow()", veaf.p(self:getName())) self.isArrow = true return self end function VeafDrawingOnMap:draw() -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("drawing line [%s] - [%s]", veaf.p(lastPoint), veaf.p(point)) local id = veaf.getUniqueIdentifier() if lastPoint then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("finishing the polygon") local id = veaf.getUniqueIdentifier() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("VeafDrawingOnMap[%s]:erase()", veaf.p(self:getName())) if self.dcsMarkerIds then for _, id in pairs(self.dcsMarkerIds) do -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("VeafCircleOnMap[%s]:setCenter(%s)", veaf.p(self.name), veaf.p(value)) self.points = { mist.utils.deepCopy(value) } return self end function VeafCircleOnMap:setRadius(value) -- LOGGING DISABLED WHEN COMPILING("VeafCircleOnMap[%s]:setRadius(%s)", veaf.p(self.name), veaf.p(value)) self.radius = value return self end function VeafCircleOnMap:draw() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("VeafSquareOnMap[%s]:setSide(%s)", veaf.p(self.name), veaf.p(value)) self.side = value self:compute() return self end function VeafSquareOnMap:compute() -- LOGGING DISABLED WHEN COMPILING("VeafSquareOnMap[%s]:compute()", veaf.p(self.name)) if self.side and self.center then -- LOGGING DISABLED WHEN COMPILING("self.center=%s", veaf.p(self.center)) -- LOGGING DISABLED WHEN COMPILING("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 } -- LOGGING DISABLED WHEN COMPILING("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 } -- LOGGING DISABLED WHEN COMPILING("rightUpPoint=%s", veaf.p(rightUpPoint)) self.points = { leftDownPoint, rightUpPoint } end return self end function VeafSquareOnMap:draw() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() --- 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) -- 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) -- LOGGING DISABLED WHEN COMPILING(message, args) end -- override the ctld logs with our own methods ---@diagnostic disable-next-line: duplicate-set-field ctld.logTrace = function(message, args) -- LOGGING DISABLED WHEN COMPILING(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-24P"] = 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 ****************** ctld.aircraftTypeTable = { "Hercules", "UH-60L", "Ka-50", "Ka-50_3", "Mi-8MT", "Mi-24P", "SA342L", "SA342M", "SA342Mistral", "SA342Minigun", "UH-1H", "CH-47Fbl1", "Yak-52", } 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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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" -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("Adding CTLD pickup zone for ship: [%s]", veaf.p(zone)) end end -- generate 20 pickup zone names in the form "pickzone #001" -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(message) end -- override the csar logs with our own methods ---@diagnostic disable-next-line: duplicate-set-field csar.logTrace = function(message) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 = "2024.10.11" -- 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"] = "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 [7] [8] = { ["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 [8] [9] = { ["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 [9] [10] = { ["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 [10] [11] = { ["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 [11] [12] = { ["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 [12] [13] = { ["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 [13] [14] = { ["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 [14] [15] = { ["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 [15] [16] = { ["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 [16] [17] = { ["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 [17] [18] = { ["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 [18] [19] = { ["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 [19] [20] = { ["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 [20] [21] = { ["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 [21] [22] = { ["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 [22] [23] = { ["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 [23] [24] = { ["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 [24] [25] = { ["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 [25] [26] = { ["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 [26] [27] = { ["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 [27] [28] = { ["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 [28] [29] = { ["type"] = "HQ-7_LN_P", ["name"] = "HQ-7 LN (Player)", ["category"] = "Air Defence", ["description"] = "HQ-7 LN (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 [29] [30] = { ["type"] = "HQ-7_LN_SP", ["name"] = "HQ-7 Self-Propelled LN", ["category"] = "Air Defence", ["description"] = "HQ-7 Self-Propelled LN", ["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 [30] [31] = { ["type"] = "HQ-7_STR_SP", ["name"] = "HQ-7 Self-Propelled STR", ["category"] = "Air Defence", ["description"] = "HQ-7 Self-Propelled STR", ["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 [31] [32] = { ["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 [32] [33] = { ["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 [33] [34] = { ["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 [34] [35] = { ["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 [35] [36] = { ["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 [36] [37] = { ["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 [37] [38] = { ["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 [38] [39] = { ["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 [39] [40] = { ["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 [40] [41] = { ["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 [41] [42] = { ["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 [42] [43] = { ["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 [43] [44] = { ["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 [44] [45] = { ["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 [45] [46] = { ["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 [46] [47] = { ["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 [47] [48] = { ["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 [48] [49] = { ["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 [49] [50] = { ["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 [50] [51] = { ["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 [51] [52] = { ["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 [52] [53] = { ["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 [53] [54] = { ["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 [54] [55] = { ["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 [55] [56] = { ["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 [56] [57] = { ["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 [57] [58] = { ["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 [58] [59] = { ["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 [59] [60] = { ["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 [60] [61] = { ["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 [61] [62] = { ["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 [62] [63] = { ["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 [63] [64] = { ["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, [74] = 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 [64] [65] = { ["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 [65] [66] = { ["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 [66] [67] = { ["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 [67] [68] = { ["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 [68] [69] = { ["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 [69] [70] = { ["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 [70] [71] = { ["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 [71] [72] = { ["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 [72] [73] = { ["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 [73] [74] = { ["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, [74] = 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 [74] [75] = { ["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 [75] [76] = { ["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 [76] [77] = { ["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 [77] [78] = { ["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 [78] [79] = { ["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 [79] [80] = { ["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 [80] [81] = { ["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 [81] [82] = { ["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 [82] [83] = { ["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 [83] [84] = { ["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 [84] [85] = { ["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 [85] [86] = { ["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 [86] [87] = { ["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 [87] [88] = { ["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 [88] [89] = { ["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 [89] [90] = { ["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 [90] [91] = { ["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 [91] [92] = { ["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 [92] [93] = { ["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 [93] [94] = { ["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 [94] [95] = { ["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 [95] [96] = { ["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 [96] [97] = { ["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 [97] [98] = { ["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 [98] [99] = { ["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 [99] [100] = { ["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 [100] [101] = { ["type"] = "Cow", ["name"] = "Cow", ["category"] = "Animal", ["description"] = "Cow", ["attribute"] = { [5] = true, [9] = true, [100] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [101] [102] = { ["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 [102] [103] = { ["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 [103] [104] = { ["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 [104] [105] = { ["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 [105] [106] = { ["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 [106] [107] = { ["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 [107] [108] = { ["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 [108] [109] = { ["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 [109] [110] = { ["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 [110] [111] = { ["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 [111] [112] = { ["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 [112] [113] = { ["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 [113] [114] = { ["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 [114] [115] = { ["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 [115] [116] = { ["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 [116] [117] = { ["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 [117] [118] = { ["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 [118] [119] = { ["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 [119] [120] = { ["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 [120] [121] = { ["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 [121] [122] = { ["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 [122] [123] = { ["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 [123] [124] = { ["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 [124] [125] = { ["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 [125] [126] = { ["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 [126] [127] = { ["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 [127] [128] = { ["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 [128] [129] = { ["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 [129] [130] = { ["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 [130] [131] = { ["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 [131] [132] = { ["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 [132] [133] = { ["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 [133] [134] = { ["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 [134] [135] = { ["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 [135] [136] = { ["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 [136] [137] = { ["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 [137] [138] = { ["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 [138] [139] = { ["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 [139] [140] = { ["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 [140] [141] = { ["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 [141] [142] = { ["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 [142] [143] = { ["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 [143] [144] = { ["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 [144] [145] = { ["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 [145] [146] = { ["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 [146] [147] = { ["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 [147] [148] = { ["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 [148] [149] = { ["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 [149] [150] = { ["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 [150] [151] = { ["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 [151] [152] = { ["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 [152] [153] = { ["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 [153] [154] = { ["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 [154] [155] = { ["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 [155] [156] = { ["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 [156] [157] = { ["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 [157] [158] = { ["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 [158] [159] = { ["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 [159] [160] = { ["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 [160] [161] = { ["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 [161] [162] = { ["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 [162] [163] = { ["type"] = "T-90", ["name"] = "MBT T-90", ["category"] = "Armor", ["description"] = "MBT T-90", ["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 [163] [164] = { ["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 [164] [165] = { ["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 [165] [166] = { ["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 [166] [167] = { ["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 [167] [168] = { ["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 [168] [169] = { ["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 [169] [170] = { ["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 [170] [171] = { ["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 [171] [172] = { ["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 [172] [173] = { ["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 [173] [174] = { ["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 [174] [175] = { ["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 [175] [176] = { ["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 [176] [177] = { ["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 [177] [178] = { ["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, ["Ground vehicles"] = true, ["Indirect fire"] = true, [17] = true, ["Armed vehicles"] = true, ["NonAndLightArmoredUnits"] = true, ["Vehicles"] = true, ["Armed ground units"] = true, ["All"] = true, ["Ground Units"] = true, [348] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [178] [179] = { ["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 [179] [180] = { ["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 [180] [181] = { ["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 [181] [182] = { ["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 [182] [183] = { ["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 [183] [184] = { ["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 [184] [185] = { ["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 [185] [186] = { ["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 [186] [187] = { ["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 [187] [188] = { ["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 [188] [189] = { ["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 [189] [190] = { ["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 [190] [191] = { ["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 [191] [192] = { ["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 [192] [193] = { ["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 [193] [194] = { ["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 [194] [195] = { ["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 [195] [196] = { ["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 [196] [197] = { ["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 [197] [198] = { ["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 [198] [199] = { ["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 [199] [200] = { ["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 [200] [201] = { ["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 [201] [202] = { ["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 [202] [203] = { ["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 [203] [204] = { ["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 [204] [205] = { ["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 [205] [206] = { ["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 [206] [207] = { ["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 [207] [208] = { ["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 [208] [209] = { ["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 [209] [210] = { ["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 [210] [211] = { ["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 [211] [212] = { ["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 [212] [213] = { ["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 [213] [214] = { ["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 [214] [215] = { ["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 [215] [216] = { ["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 [216] [217] = { ["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 [217] [218] = { ["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 [218] [219] = { ["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 [219] [220] = { ["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 [220] [221] = { ["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 [221] [222] = { ["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 [222] [223] = { ["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 [223] [224] = { ["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 [224] [225] = { ["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 [225] [226] = { ["type"] = "big_smoke", ["name"] = "Big smoke", ["category"] = "Effect", ["description"] = "Big smoke", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [226] [227] = { ["type"] = ".Command Center", ["name"] = "Command Center", ["category"] = "Fortification", ["description"] = "Command Center", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [227] [228] = { ["type"] = "345 Excavator", ["name"] = "Excavator", ["category"] = "Fortification", ["description"] = "Excavator", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [228] [229] = { ["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 [229] [230] = { ["type"] = "Airshow_Cone", ["name"] = "Airshow cone", ["category"] = "Fortification", ["description"] = "Airshow cone", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [230] [231] = { ["type"] = "Airshow_Crowd", ["name"] = "Airshow Crowd", ["category"] = "Fortification", ["description"] = "Airshow Crowd", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [231] [232] = { ["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 [232] [233] = { ["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 [233] [234] = { ["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 [234] [235] = { ["type"] = "B600", ["name"] = "M92 B600", ["category"] = "Fortification", ["description"] = "M92 B600", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [235] [236] = { ["type"] = "Barracks 2", ["name"] = "Barracks 2", ["category"] = "Fortification", ["description"] = "Barracks 2", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [236] [237] = { ["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 [237] [238] = { ["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 [238] [239] = { ["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 [239] [240] = { ["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 [240] [241] = { ["type"] = "Beer Bomb", ["name"] = "Barrel", ["category"] = "Fortification", ["description"] = "Barrel", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [241] [242] = { ["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 [242] [243] = { ["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 [243] [244] = { ["type"] = "Black_Tyre", ["name"] = "Mark Tyre Black", ["category"] = "Fortification", ["description"] = "Mark Tyre Black", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [244] [245] = { ["type"] = "Black_Tyre_RF", ["name"] = "Mark Tyre with Red Flag", ["category"] = "Fortification", ["description"] = "Mark Tyre with Red Flag", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [245] [246] = { ["type"] = "Black_Tyre_WF", ["name"] = "Mark Tyre with White Flag", ["category"] = "Fortification", ["description"] = "Mark Tyre with White Flag", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [246] [247] = { ["type"] = "Boiler-house A", ["name"] = "Boiler-house A", ["category"] = "Fortification", ["description"] = "Boiler-house A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [247] [248] = { ["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 [248] [249] = { ["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 [249] [250] = { ["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 [250] [251] = { ["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 [251] [252] = { ["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 [252] [253] = { ["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 [253] [254] = { ["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 [254] [255] = { ["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 [255] [256] = { ["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 [256] [257] = { ["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 [257] [258] = { ["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 [258] [259] = { ["type"] = "Cafe", ["name"] = "Cafe", ["category"] = "Fortification", ["description"] = "Cafe", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [259] [260] = { ["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 [260] [261] = { ["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 [261] [262] = { ["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 [262] [263] = { ["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 [263] [264] = { ["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 [264] [265] = { ["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 [265] [266] = { ["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 [266] [267] = { ["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 [267] [268] = { ["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 [268] [269] = { ["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 [269] [270] = { ["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 [270] [271] = { ["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 [271] [272] = { ["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 [272] [273] = { ["type"] = "Chemical tank A", ["name"] = "Chemical tank A", ["category"] = "Fortification", ["description"] = "Chemical tank A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [273] [274] = { ["type"] = "Comms tower M", ["name"] = "Comms tower M", ["category"] = "Fortification", ["description"] = "Comms tower M", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [274] [275] = { ["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 [275] [276] = { ["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 [276] [277] = { ["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 [277] [278] = { ["type"] = "Container brown", ["name"] = "Container brown", ["category"] = "Fortification", ["description"] = "Container brown", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [278] [279] = { ["type"] = "Container red 1", ["name"] = "Container red 1", ["category"] = "Fortification", ["description"] = "Container red 1", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [279] [280] = { ["type"] = "Container red 2", ["name"] = "Container red 2", ["category"] = "Fortification", ["description"] = "Container red 2", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [280] [281] = { ["type"] = "Container red 3", ["name"] = "Container red 3", ["category"] = "Fortification", ["description"] = "Container red 3", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [281] [282] = { ["type"] = "Container white", ["name"] = "Container white", ["category"] = "Fortification", ["description"] = "Container white", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [282] [283] = { ["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 [283] [284] = { ["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 [284] [285] = { ["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 [285] [286] = { ["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 [286] [287] = { ["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 [287] [288] = { ["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 [288] [289] = { ["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 [289] [290] = { ["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 [290] [291] = { ["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 [291] [292] = { ["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 [292] [293] = { ["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 [293] [294] = { ["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 [294] [295] = { ["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 [295] [296] = { ["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 [296] [297] = { ["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 [297] [298] = { ["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 [298] [299] = { ["type"] = "Electric power box", ["name"] = "Electric power box", ["category"] = "Fortification", ["description"] = "Electric power box", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [299] [300] = { ["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 [300] [301] = { ["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 [301] [302] = { ["type"] = "Farm A", ["name"] = "Farm A", ["category"] = "Fortification", ["description"] = "Farm A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [302] [303] = { ["type"] = "Farm B", ["name"] = "Farm B", ["category"] = "Fortification", ["description"] = "Farm B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [303] [304] = { ["type"] = "FARP Ammo Dump Coating", ["name"] = "FARP Ammo Storage", ["category"] = "Fortification", ["description"] = "FARP Ammo Storage", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [304] [305] = { ["type"] = "FARP CP Blindage", ["name"] = "FARP Command Post", ["category"] = "Fortification", ["description"] = "FARP Command Post", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [305] [306] = { ["type"] = "FARP Fuel Depot", ["name"] = "FARP Fuel Depot", ["category"] = "Fortification", ["description"] = "FARP Fuel Depot", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [306] [307] = { ["type"] = "FARP Tent", ["name"] = "FARP Tent", ["category"] = "Fortification", ["description"] = "FARP Tent", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [307] [308] = { ["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 [308] [309] = { ["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 [309] [310] = { ["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 [310] [311] = { ["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 [311] [312] = { ["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 [312] [313] = { ["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 [313] [314] = { ["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 [314] [315] = { ["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 [315] [316] = { ["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 [316] [317] = { ["type"] = "FlagPole", ["name"] = "Flag Pole", ["category"] = "Fortification", ["description"] = "Flag Pole", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [317] [318] = { ["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 [318] [319] = { ["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 [319] [320] = { ["type"] = "Fuel tank", ["name"] = "Fuel tank", ["category"] = "Fortification", ["description"] = "Fuel tank", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [320] [321] = { ["type"] = "Garage A", ["name"] = "Garage A", ["category"] = "Fortification", ["description"] = "Garage A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [321] [322] = { ["type"] = "Garage B", ["name"] = "Garage B", ["category"] = "Fortification", ["description"] = "Garage B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [322] [323] = { ["type"] = "Garage small A", ["name"] = "Garage small A", ["category"] = "Fortification", ["description"] = "Garage small A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [323] [324] = { ["type"] = "Garage small B", ["name"] = "Garage small B", ["category"] = "Fortification", ["description"] = "Garage small B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [324] [325] = { ["type"] = "GeneratorF", ["name"] = "GeneratorF", ["category"] = "Fortification", ["description"] = "GeneratorF", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [325] [326] = { ["type"] = "Hangar A", ["name"] = "Hangar A", ["category"] = "Fortification", ["description"] = "Hangar A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [326] [327] = { ["type"] = "Hangar B", ["name"] = "Hangar B", ["category"] = "Fortification", ["description"] = "Hangar B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [327] [328] = { ["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 [328] [329] = { ["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 [329] [330] = { ["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 [330] [331] = { ["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 [331] [332] = { ["type"] = "Hemmkurvenhindernis", ["name"] = "Hemmkurvenhindernis", ["category"] = "Fortification", ["description"] = "Hemmkurvenhindernis", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [332] [333] = { ["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 [333] [334] = { ["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 [334] [335] = { ["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 [335] [336] = { ["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 [336] [337] = { ["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 [337] [338] = { ["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 [338] [339] = { ["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 [339] [340] = { ["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 [340] [341] = { ["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 [341] [342] = { ["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 [342] [343] = { ["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 [343] [344] = { ["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 [344] [345] = { ["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 [345] [346] = { ["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 [346] [347] = { ["type"] = "Jerrycan", ["name"] = "M92 Jerrycan", ["category"] = "Fortification", ["description"] = "M92 Jerrycan", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [347] [348] = { ["type"] = "Ladder", ["name"] = "M92 Ladder", ["category"] = "Fortification", ["description"] = "M92 Ladder", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [348] [349] = { ["type"] = "Landmine", ["name"] = "Landmine", ["category"] = "Fortification", ["description"] = "Landmine", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [349] [350] = { ["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 [350] [351] = { ["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 [351] [352] = { ["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 [352] [353] = { ["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 [353] [354] = { ["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 [354] [355] = { ["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 [355] [356] = { ["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 [356] [357] = { ["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 [357] [358] = { ["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 [358] [359] = { ["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 [359] [360] = { ["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 [360] [361] = { ["type"] = "Military staff", ["name"] = "Military staff", ["category"] = "Fortification", ["description"] = "Military staff", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [361] [362] = { ["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 [362] [363] = { ["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 [363] [364] = { ["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 [364] [365] = { ["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 [365] [366] = { ["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 [366] [367] = { ["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 [367] [368] = { ["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 [368] [369] = { ["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 [369] [370] = { ["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 [370] [371] = { ["type"] = "Oil derrick", ["name"] = "Oil derrick", ["category"] = "Fortification", ["description"] = "Oil derrick", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [371] [372] = { ["type"] = "Oil platform", ["name"] = "Oil platform", ["category"] = "Fortification", ["description"] = "Oil platform", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [372] [373] = { ["type"] = "Orca", ["name"] = "Orca Whale", ["category"] = "Fortification", ["description"] = "Orca Whale", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [373] [374] = { ["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 [374] [375] = { ["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 [375] [376] = { ["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 [376] [377] = { ["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 [377] [378] = { ["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 [378] [379] = { ["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 [379] [380] = { ["type"] = "Pump station", ["name"] = "Pump station", ["category"] = "Fortification", ["description"] = "Pump station", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [380] [381] = { ["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 [381] [382] = { ["type"] = "Railway crossing A", ["name"] = "Railway crossing A", ["category"] = "Fortification", ["description"] = "Railway crossing A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [382] [383] = { ["type"] = "Railway crossing B", ["name"] = "Railway crossing B", ["category"] = "Fortification", ["description"] = "Railway crossing B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [383] [384] = { ["type"] = "Railway station", ["name"] = "Railway station", ["category"] = "Fortification", ["description"] = "Railway station", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [384] [385] = { ["type"] = "Red_Flag", ["name"] = "Mark Flag Red", ["category"] = "Fortification", ["description"] = "Mark Flag Red", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [385] [386] = { ["type"] = "Repair workshop", ["name"] = "Repair workshop", ["category"] = "Fortification", ["description"] = "Repair workshop", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [386] [387] = { ["type"] = "Restaurant 1", ["name"] = "Restaurant 1", ["category"] = "Fortification", ["description"] = "Restaurant 1", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [387] [388] = { ["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 [388] [389] = { ["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 [389] [390] = { ["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 [390] [391] = { ["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 [391] [392] = { ["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 [392] [393] = { ["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 [393] [394] = { ["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 [394] [395] = { ["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 [395] [396] = { ["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 [396] [397] = { ["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 [397] [398] = { ["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 [398] [399] = { ["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 [399] [400] = { ["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 [400] [401] = { ["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 [401] [402] = { ["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 [402] [403] = { ["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 [403] [404] = { ["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 [404] [405] = { ["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 [405] [406] = { ["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 [406] [407] = { ["type"] = "Shelter", ["name"] = "Shelter", ["category"] = "Fortification", ["description"] = "Shelter", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [407] [408] = { ["type"] = "Shelter B", ["name"] = "Shelter B", ["category"] = "Fortification", ["description"] = "Shelter B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [408] [409] = { ["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 [409] [410] = { ["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 [410] [411] = { ["type"] = "Shop", ["name"] = "Shop", ["category"] = "Fortification", ["description"] = "Shop", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [411] [412] = { ["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 [412] [413] = { ["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 [413] [414] = { ["type"] = "Small house 1A", ["name"] = "Small house 1A", ["category"] = "Fortification", ["description"] = "Small house 1A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [414] [415] = { ["type"] = "Small house 1A area", ["name"] = "Small house 1A area", ["category"] = "Fortification", ["description"] = "Small house 1A area", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [415] [416] = { ["type"] = "Small house 1B", ["name"] = "Small house 1B", ["category"] = "Fortification", ["description"] = "Small house 1B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [416] [417] = { ["type"] = "Small house 1B area", ["name"] = "Small house 1B area", ["category"] = "Fortification", ["description"] = "Small house 1B area", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [417] [418] = { ["type"] = "Small house 1C area", ["name"] = "Small house 1C area", ["category"] = "Fortification", ["description"] = "Small house 1C area", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [418] [419] = { ["type"] = "Small house 2C", ["name"] = "Small house 2C", ["category"] = "Fortification", ["description"] = "Small house 2C", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [419] [420] = { ["type"] = "Small werehouse 1", ["name"] = "Small warehouse 1", ["category"] = "Fortification", ["description"] = "Small warehouse 1", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [420] [421] = { ["type"] = "Small werehouse 2", ["name"] = "Small warehouse 2", ["category"] = "Fortification", ["description"] = "Small warehouse 2", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [421] [422] = { ["type"] = "Small werehouse 3", ["name"] = "Small warehouse 3", ["category"] = "Fortification", ["description"] = "Small warehouse 3", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [422] [423] = { ["type"] = "Small werehouse 4", ["name"] = "Small warehouse 4", ["category"] = "Fortification", ["description"] = "Small warehouse 4", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [423] [424] = { ["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 [424] [425] = { ["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 [425] [426] = { ["type"] = "Subsidiary structure 1", ["name"] = "Subsidiary structure 1", ["category"] = "Fortification", ["description"] = "Subsidiary structure 1", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [426] [427] = { ["type"] = "Subsidiary structure 2", ["name"] = "Subsidiary structure 2", ["category"] = "Fortification", ["description"] = "Subsidiary structure 2", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [427] [428] = { ["type"] = "Subsidiary structure 3", ["name"] = "Subsidiary structure 3", ["category"] = "Fortification", ["description"] = "Subsidiary structure 3", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [428] [429] = { ["type"] = "Subsidiary structure A", ["name"] = "Subsidiary structure A", ["category"] = "Fortification", ["description"] = "Subsidiary structure A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [429] [430] = { ["type"] = "Subsidiary structure B", ["name"] = "Subsidiary structure B", ["category"] = "Fortification", ["description"] = "Subsidiary structure B", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [430] [431] = { ["type"] = "Subsidiary structure C", ["name"] = "Subsidiary structure C", ["category"] = "Fortification", ["description"] = "Subsidiary structure C", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [431] [432] = { ["type"] = "Subsidiary structure D", ["name"] = "Subsidiary structure D", ["category"] = "Fortification", ["description"] = "Subsidiary structure D", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [432] [433] = { ["type"] = "Subsidiary structure E", ["name"] = "Subsidiary structure E", ["category"] = "Fortification", ["description"] = "Subsidiary structure E", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [433] [434] = { ["type"] = "Subsidiary structure F", ["name"] = "Subsidiary structure F", ["category"] = "Fortification", ["description"] = "Subsidiary structure F", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [434] [435] = { ["type"] = "Subsidiary structure G", ["name"] = "Subsidiary structure G", ["category"] = "Fortification", ["description"] = "Subsidiary structure G", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [435] [436] = { ["type"] = "Supermarket A", ["name"] = "Supermarket A", ["category"] = "Fortification", ["description"] = "Supermarket A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [436] [437] = { ["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 [437] [438] = { ["type"] = "Tech combine", ["name"] = "Tech combine", ["category"] = "Fortification", ["description"] = "Tech combine", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [438] [439] = { ["type"] = "Tech hangar A", ["name"] = "Tech hangar A", ["category"] = "Fortification", ["description"] = "Tech hangar A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [439] [440] = { ["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 [440] [441] = { ["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 [441] [442] = { ["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 [442] [443] = { ["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 [443] [444] = { ["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 [444] [445] = { ["type"] = "Tetrahydra", ["name"] = "Tetrahydra", ["category"] = "Fortification", ["description"] = "Tetrahydra", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [445] [446] = { ["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 [446] [447] = { ["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 [447] [448] = { ["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 [448] [449] = { ["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 [449] [450] = { ["type"] = "TV tower", ["name"] = "TV tower", ["category"] = "Fortification", ["description"] = "TV tower", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [450] [451] = { ["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 [451] [452] = { ["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 [452] [453] = { ["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 [453] [454] = { ["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 [454] [455] = { ["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 [455] [456] = { ["type"] = "Water tower A", ["name"] = "Water tower A", ["category"] = "Fortification", ["description"] = "Water tower A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [456] [457] = { ["type"] = "WC", ["name"] = "WC", ["category"] = "Fortification", ["description"] = "WC", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [457] [458] = { ["type"] = "White_Flag", ["name"] = "Mark Flag White", ["category"] = "Fortification", ["description"] = "Mark Flag White", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [458] [459] = { ["type"] = "White_Tyre", ["name"] = "Mark Tyre White", ["category"] = "Fortification", ["description"] = "Mark Tyre White", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [459] [460] = { ["type"] = "Windsock", ["name"] = "Windsock", ["category"] = "Fortification", ["description"] = "Windsock", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [460] [461] = { ["type"] = "WindTurbine", ["name"] = "Wind Turbine", ["category"] = "Fortification", ["description"] = "Wind Turbine", ["attribute"] = { [5] = true, [9] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [461] [462] = { ["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 [462] [463] = { ["type"] = "Workshop A", ["name"] = "Workshop A", ["category"] = "Fortification", ["description"] = "Workshop A", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [463] [464] = { ["type"] = "GrassAirfield", ["name"] = "Grass Airfield", ["category"] = "GrassAirfield", ["description"] = "Grass Airfield", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [464] [465] = { ["type"] = "Bridge", ["name"] = "Bridge", ["category"] = "GroundObject", ["description"] = "Bridge", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [465] [466] = { ["type"] = "Building", ["name"] = "Building", ["category"] = "GroundObject", ["description"] = "Building", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [466] [467] = { ["type"] = "Train", ["name"] = "Train", ["category"] = "GroundObject", ["description"] = "Train", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [467] [468] = { ["type"] = "Transport", ["name"] = "Transport", ["category"] = "GroundObject", ["description"] = "Transport", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [468] [469] = { ["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 [469] [470] = { ["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 [470] [471] = { ["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 [471] [472] = { ["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 [472] [473] = { ["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 [473] [474] = { ["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 [474] [475] = { ["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 [475] [476] = { ["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 [476] [477] = { ["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 [477] [478] = { ["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 [478] [479] = { ["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 [479] [480] = { ["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 [480] [481] = { ["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 [481] [482] = { ["air"] = true, ["type"] = "Mi-28N", ["name"] = "Mi-28N", ["category"] = "Helicopter", ["description"] = "Mi-28N", ["attribute"] = { [1] = true, [2] = true, [6] = true, ["All"] = true, ["NonAndLightArmoredUnits"] = true, ["Air"] = true, ["NonArmoredUnits"] = true, [167] = true, ["Helicopters"] = true, ["Attack helicopters"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [482] [483] = { ["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 [483] [484] = { ["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 [484] [485] = { ["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 [485] [486] = { ["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 [486] [487] = { ["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 [487] [488] = { ["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 [488] [489] = { ["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 [489] [490] = { ["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 [490] [491] = { ["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 [491] [492] = { ["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 [492] [493] = { ["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 [493] [494] = { ["type"] = "FARP", ["name"] = "FARP", ["category"] = "Heliport", ["isPutToWater"] = true, ["description"] = "FARP", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [494] [495] = { ["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 [495] [496] = { ["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 [496] [497] = { ["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 [497] [498] = { ["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 [498] [499] = { ["type"] = "SINGLE_HELIPAD", ["name"] = "Helipad Single", ["category"] = "Heliport", ["isPutToWater"] = true, ["description"] = "Helipad Single", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [499] [500] = { ["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 [500] [501] = { ["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 [501] [502] = { ["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 [502] [503] = { ["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 [503] [504] = { ["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 [504] [505] = { ["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 [505] [506] = { ["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 [506] [507] = { ["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 [507] [508] = { ["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 [508] [509] = { ["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 [509] [510] = { ["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 [510] [511] = { ["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 [511] [512] = { ["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 [512] [513] = { ["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 [513] [514] = { ["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 [514] [515] = { ["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 [515] [516] = { ["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 [516] [517] = { ["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 [517] [518] = { ["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 [518] [519] = { ["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 [519] [520] = { ["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 [520] [521] = { ["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 [521] [522] = { ["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 [522] [523] = { ["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 [523] [524] = { ["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 [524] [525] = { ["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 [525] [526] = { ["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 [526] [527] = { ["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 [527] [528] = { ["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 [528] [529] = { ["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 [529] [530] = { ["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 [530] [531] = { ["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 [531] [532] = { ["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 [532] [533] = { ["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 [533] [534] = { ["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 [534] [535] = { ["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 [535] [536] = { ["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 [536] [537] = { ["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 [537] [538] = { ["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 [538] [539] = { ["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 [539] [540] = { ["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 [540] [541] = { ["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 [541] [542] = { ["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 [542] [543] = { ["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 [543] [544] = { ["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 [544] [545] = { ["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 [545] [546] = { ["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 [546] [547] = { ["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 [547] [548] = { ["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 [548] [549] = { ["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 [549] [550] = { ["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 [550] [551] = { ["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 [551] [552] = { ["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 [552] [553] = { ["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 [553] [554] = { ["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 [554] [555] = { ["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 [555] [556] = { ["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 [556] [557] = { ["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 [557] [558] = { ["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 [558] [559] = { ["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 [559] [560] = { ["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 [560] [561] = { ["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 [561] [562] = { ["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 [562] [563] = { ["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 [563] [564] = { ["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 [564] [565] = { ["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 [565] [566] = { ["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 [566] [567] = { ["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 [567] [568] = { ["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 [568] [569] = { ["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 [569] [570] = { ["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 [570] [571] = { ["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 [571] [572] = { ["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 [572] [573] = { ["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 [573] [574] = { ["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 [574] [575] = { ["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 [575] [576] = { ["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 [576] [577] = { ["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 [577] [578] = { ["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 [578] [579] = { ["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 [579] [580] = { ["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 [580] [581] = { ["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 [581] [582] = { ["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 [582] [583] = { ["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 [583] [584] = { ["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 [584] [585] = { ["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 [585] [586] = { ["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 [586] [587] = { ["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 [587] [588] = { ["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 [588] [589] = { ["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 [589] [590] = { ["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 [590] [591] = { ["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 [591] [592] = { ["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 [592] [593] = { ["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 [593] [594] = { ["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 [594] [595] = { ["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 [595] [596] = { ["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 [596] [597] = { ["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 [597] [598] = { ["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 [598] [599] = { ["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 [599] [600] = { ["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 [600] [601] = { ["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 [601] [602] = { ["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 [602] [603] = { ["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 [603] [604] = { ["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 [604] [605] = { ["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 [605] [606] = { ["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 [606] [607] = { ["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 [607] [608] = { ["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 [608] [609] = { ["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 [609] [610] = { ["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 [610] [611] = { ["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 [611] [612] = { ["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 [612] [613] = { ["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 [613] [614] = { ["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 [614] [615] = { ["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 [615] [616] = { ["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 [616] [617] = { ["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 [617] [618] = { ["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 [618] [619] = { ["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 [619] [620] = { ["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 [620] [621] = { ["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 [621] [622] = { ["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 [622] [623] = { ["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 [623] [624] = { ["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 [624] [625] = { ["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 [625] [626] = { ["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 [626] [627] = { ["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 [627] [628] = { ["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 [628] [629] = { ["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 [629] [630] = { ["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 [630] [631] = { ["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 [631] [632] = { ["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 [632] [633] = { ["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 [633] [634] = { ["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, [4] = true, [297] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["All"] = true, ["Planes"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [634] [635] = { ["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 [635] [636] = { ["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 [636] [637] = { ["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 [637] [638] = { ["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 [638] [639] = { ["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 [639] [640] = { ["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 [640] [641] = { ["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 [641] [642] = { ["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 [642] [643] = { ["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 [643] [644] = { ["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 [644] [645] = { ["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 [645] [646] = { ["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 [646] [647] = { ["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 [647] [648] = { ["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 [648] [649] = { ["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 [649] [650] = { ["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 [650] [651] = { ["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 [651] [652] = { ["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 [652] [653] = { ["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 [653] [654] = { ["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 [654] [655] = { ["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 [655] [656] = { ["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 [656] [657] = { ["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 [657] [658] = { ["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 [658] [659] = { ["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 [659] [660] = { ["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 [660] [661] = { ["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 [661] [662] = { ["air"] = true, ["type"] = "Tu-95MS", ["name"] = "Tu-95MS", ["category"] = "Plane", ["description"] = "Tu-95MS", ["attribute"] = { [1] = true, ["Air"] = true, ["Refuelable"] = true, [4] = true, ["Strategic bombers"] = true, ["Battle airplanes"] = true, ["NonAndLightArmoredUnits"] = true, ["Bombers"] = true, [21] = true, ["All"] = true, ["Planes"] = true, ["NonArmoredUnits"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [662] [663] = { ["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 [663] [664] = { ["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 [664] [665] = { ["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 [665] [666] = { ["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 [666] [667] = { ["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 [667] [668] = { ["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 [668] [669] = { ["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 [669] [670] = { ["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 [670] [671] = { ["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 [671] [672] = { ["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 [672] [673] = { ["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 [673] [674] = { ["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 [674] [675] = { ["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 [675] [676] = { ["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 [676] [677] = { ["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 [677] [678] = { ["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 [678] [679] = { ["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 [679] [680] = { ["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 [680] [681] = { ["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 [681] [682] = { ["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 [682] [683] = { ["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 [683] [684] = { ["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 [684] [685] = { ["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 [685] [686] = { ["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 [686] [687] = { ["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 [687] [688] = { ["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 [688] [689] = { ["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 [689] [690] = { ["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 [690] [691] = { ["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 [691] [692] = { ["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 [692] [693] = { ["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 [693] [694] = { ["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 [694] [695] = { ["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 [695] [696] = { ["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 [696] [697] = { ["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 [697] [698] = { ["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 [698] [699] = { ["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 [699] [700] = { ["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 [700] [701] = { ["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 [701] [702] = { ["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 [702] [703] = { ["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 [703] [704] = { ["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 [704] [705] = { ["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 [705] [706] = { ["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 [706] [707] = { ["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 [707] [708] = { ["type"] = "Type_052B", ["name"] = "Type 052B Destroyer", ["category"] = "Ship", ["naval"] = true, ["description"] = "Type 052B Destroyer", ["attribute"] = { [270] = true, ["Ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, [13] = true, ["Armed ships"] = true, ["Armed Air Defence"] = true, ["Destroyers"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, ["HelicopterCarrier"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [708] [709] = { ["type"] = "Type_052C", ["name"] = "Type 052C Destroyer", ["category"] = "Ship", ["naval"] = true, ["description"] = "Type 052C Destroyer", ["attribute"] = { ["Ships"] = true, ["RADAR_BAND1_FOR_ARM"] = true, [272] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, [13] = true, ["Cruisers"] = true, ["Armed Air Defence"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, ["Armed ships"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [709] [710] = { ["type"] = "Type_054A", ["name"] = "Type 054A Frigate", ["category"] = "Ship", ["naval"] = true, ["description"] = "Type 054A Frigate", ["attribute"] = { ["Ships"] = true, [271] = true, ["RADAR_BAND1_FOR_ARM"] = true, ["RADAR_BAND2_FOR_ARM"] = true, ["DetectionByAWACS"] = true, [3] = true, [12] = true, ["Heavy armed ships"] = true, [13] = true, ["Armed ships"] = true, ["Armed Air Defence"] = true, ["HelicopterCarrier"] = true, ["Armed Ship"] = true, ["HeavyArmoredUnits"] = true, ["All"] = true, ["Naval"] = true, ["Frigates"] = true, }, -- end of ["attribute"] ["aliases"] = { }, -- end of ["aliases"] }, -- end of [710] [711] = { ["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 [711] [712] = { ["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 [712] [713] = { ["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 [713] [714] = { ["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 [714] [715] = { ["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 [715] [716] = { ["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 [716] [717] = { ["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 [717] [718] = { ["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 [718] [719] = { ["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 [719] [720] = { ["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 [720] [721] = { ["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 [721] [722] = { ["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 [722] [723] = { ["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 [723] [724] = { ["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 [724] [725] = { ["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 [725] [726] = { ["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 [726] [727] = { ["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 [727] [728] = { ["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 [728] [729] = { ["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 [729] [730] = { ["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 [730] [731] = { ["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 [731] [732] = { ["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 [732] [733] = { ["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 [733] [734] = { ["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 [734] [735] = { ["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 [735] [736] = { ["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 [736] [737] = { ["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 [737] [738] = { ["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 [738] [739] = { ["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 [739] [740] = { ["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 [740] [741] = { ["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 [741] [742] = { ["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 [742] [743] = { ["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 [743] [744] = { ["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 [744] [745] = { ["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 [745] [746] = { ["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 [746] [747] = { ["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 [747] [748] = { ["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 [748] [749] = { ["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 [749] [750] = { ["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 [750] [751] = { ["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 [751] [752] = { ["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 [752] [753] = { ["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 [753] [754] = { ["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 [754] [755] = { ["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 [755] [756] = { ["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 [756] [757] = { ["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 [757] [758] = { ["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 [758] [759] = { ["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 [759] [760] = { ["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 [760] [761] = { ["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 [761] [762] = { ["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 [762] [763] = { ["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 [763] [764] = { ["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 [764] [765] = { ["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 [765] [766] = { ["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 [766] [767] = { ["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 [767] [768] = { ["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 [768] [769] = { ["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 [769] [770] = { ["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 [770] [771] = { ["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 [771] [772] = { ["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 [772] [773] = { ["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 [773] [774] = { ["type"] = ".Ammunition depot", ["name"] = "Ammunition depot", ["category"] = "Warehouse", ["description"] = "Ammunition depot", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [774] [775] = { ["type"] = "Tank", ["name"] = "Tank 1", ["category"] = "Warehouse", ["description"] = "Tank 1", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [775] [776] = { ["type"] = "Tank 2", ["name"] = "Tank 2", ["category"] = "Warehouse", ["description"] = "Tank 2", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [776] [777] = { ["type"] = "Tank 3", ["name"] = "Tank 3", ["category"] = "Warehouse", ["description"] = "Tank 3", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [777] [778] = { ["type"] = "Warehouse", ["name"] = "Warehouse", ["category"] = "Warehouse", ["description"] = "Warehouse", ["aliases"] = { }, -- end of ["aliases"] }, -- end of [778] } -- 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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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.2.1" -- trace level, specific to this module --veafEventHandler.LogLevel = "trace" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- 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) -- LOGGING DISABLED WHEN COMPILING("veafEventHandler.completeUnitFromName(unitName=%s)", veaf.p(unitName)) if unitName ~= nil then local unitType = nil local unitLifePercent = nil local unit = Unit.getByName(unitName) if unit and unit.getTypeName then unitType = unit:getTypeName() local unitLife = unit:getLife() 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 end local unitPilotName = nil local unitPilotUcid = nil local unitPilot = veafRemote.getRemoteUserFromUnit(unitName) if unitPilot then -- LOGGING DISABLED WHEN COMPILING("unitPilot=%s", veaf.p(unitPilot)) unitPilotName = unitPilot.name unitPilotUcid = unitPilot.ucid end return { unitName = unitName, unitType = unitType, 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) -- LOGGING DISABLED WHEN COMPILING("veafEventHandler.addCallback(name=[%s])", veaf.p(name)) 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.knownEventsNames = { [00] = "S_EVENT_INVALID", [01] = "S_EVENT_SHOT", [02] = "S_EVENT_HIT", [03] = "S_EVENT_TAKEOFF", [04] = "S_EVENT_LAND", [05] = "S_EVENT_CRASH", [06] = "S_EVENT_EJECTION", [07] = "S_EVENT_REFUELING", [08] = "S_EVENT_DEAD", [09] = "S_EVENT_PILOT_DEAD", [10] = "S_EVENT_BASE_CAPTURED", [11] = "S_EVENT_MISSION_START", [12] = "S_EVENT_MISSION_END", [13] = "S_EVENT_TOOK_CONTROL", [14] = "S_EVENT_REFUELING_STOP", [15] = "S_EVENT_BIRTH", [16] = "S_EVENT_HUMAN_FAILURE", [17] = "S_EVENT_DETAILED_FAILURE", [18] = "S_EVENT_ENGINE_STARTUP", [19] = "S_EVENT_ENGINE_SHUTDOWN", [20] = "S_EVENT_PLAYER_ENTER_UNIT", [21] = "S_EVENT_PLAYER_LEAVE_UNIT", [22] = "S_EVENT_PLAYER_COMMENT", [23] = "S_EVENT_SHOOTING_START", [24] = "S_EVENT_SHOOTING_END", [25] = "S_EVENT_MARK_ADDED", [26] = "S_EVENT_MARK_CHANGE", [27] = "S_EVENT_MARK_REMOVED", [28] = "S_EVENT_KILL", [29] = "S_EVENT_SCORE", [30] = "S_EVENT_UNIT_LOST", [31] = "S_EVENT_LANDING_AFTER_EJECTION", [32] = "S_EVENT_PARATROOPER_LENDING", [33] = "S_EVENT_DISCARD_CHAIR_AFTER_EJECTION", [34] = "S_EVENT_WEAPON_ADD", [35] = "S_EVENT_TRIGGER_ZONE", [36] = "S_EVENT_LANDING_QUALITY_MARK", [37] = "S_EVENT_BDA", [38] = "S_EVENT_AI_ABORT_MISSION", [39] = "S_EVENT_DAYNIGHT", [40] = "S_EVENT_FLIGHT_TIME", [41] = "S_EVENT_PLAYER_SELF_KILL_PILOT", [42] = "S_EVENT_PLAYER_CAPTURE_AIRFIELD", [43] = "S_EVENT_EMERGENCY_LANDING", [44] = "S_EVENT_UNIT_CREATE_TASK", [45] = "S_EVENT_UNIT_DELETE_TASK", [46] = "S_EVENT_SIMULATION_START", [47] = "S_EVENT_WEAPON_REARM", [48] = "S_EVENT_WEAPON_DROP", [49] = "S_EVENT_UNIT_TASK_TIMEOUT", [50] = "S_EVENT_UNIT_TASK_STAGE", [51] = "S_EVENT_MAX", [52] = "[UNKNOWN]", -- ??? [53] = "[UNKNOWN]", -- ??? [54] = "S_EVENT_RUNWAY_TAKEOFF", -- since 2.9.6 [55] = "S_EVENT_RUNWAY_TOUCH", -- since 2.9.6 } veafEventHandler.knownEvents = {} -- will be set at initialisation veafEventHandler.unknownEvents = {} -- will be used to remember already signaled unknown events function veafEventHandler.checkEventKnown(eventNameOrId, warnOnly) 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 the VEAF Recorder: [%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 --- 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 -- LOGGING DISABLED WHEN COMPILING("event = %s", veaf.p(event)) -- LOGGING DISABLED WHEN COMPILING("_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 -- LOGGING DISABLED WHEN COMPILING("calling callback %s", veaf.p(callback.name)) callback.call(_event) end end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Other functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafEventHandler.initialize() -- LOGGING DISABLED WHEN COMPILING("veafEventHandler.initialize()") -- prepare the events maps for eventId, eventName in pairs(veafEventHandler.knownEventsNames) do local event = { name = eventName, id = eventId, enabled = true --false } veafEventHandler.knownEvents[eventName] = event veafEventHandler.knownEvents[eventId] = event end -- 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 -- LOGGING DISABLED WHEN COMPILING("S_EVENT_MARK_ADDED") elseif Event.id == world.event.S_EVENT_MARK_CHANGE then -- LOGGING DISABLED WHEN COMPILING("S_EVENT_MARK_CHANGE") elseif Event.id == world.event.S_EVENT_MARK_REMOVED then -- LOGGING DISABLED WHEN COMPILING("S_EVENT_MARK_REMOVED") end -- LOGGING DISABLED WHEN COMPILING(string.format("Event id = %s", tostring(Event.id))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event time = %s", tostring(Event.time))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event idx = %s", tostring(Event.idx))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event coalition = %s", tostring(Event.coalition))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event group id = %s", tostring(Event.groupID))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event pos X = %s", tostring(Event.pos.x))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event pos Y = %s", tostring(Event.pos.y))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event pos Z = %s", tostring(Event.pos.z))) if Event.initiator ~= nil and Event.initiator.getName then local _unitname = Event.initiator:getName() -- LOGGING DISABLED WHEN COMPILING(string.format("Event ini unit = %s", tostring(_unitname))) end -- LOGGING DISABLED WHEN COMPILING(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] -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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.1" -- 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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(message) return true end if command == nil then return end if position == nil then return end -- LOGGING DISABLED WHEN COMPILING(string.format("veafInterpreter.execute([%s],[%s])", command, veaf.vecToString(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 -- LOGGING DISABLED WHEN COMPILING(string.format("found an interpretable command : [%s]", command)) local unit = Unit.getByName(unitName) if unit then local position = unit:getPosition().p -- LOGGING DISABLED WHEN COMPILING(string.format("found the unit at : [%s]", veaf.vecToString(position))) local groupName = unit:getGroup():getName() -- LOGGING DISABLED WHEN COMPILING(string.format("in [%s]", groupName)) local route = mist.getGroupRoute(groupName, 'task') -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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.13.1" -- 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 -- maximum size for radio menu veafRadio.MAXIMUM_SIZE = 99999 -- 4200 -- 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 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. -- LOGGING DISABLED WHEN COMPILING(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafRadio.executeCommand(eventPos, eventText, eventCoalition, bypassSecurity) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword message = %s", tostring(val))) switch.message = val elseif key:lower() == "path" then -- Set path. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword path = %s", tostring(val))) switch.path = val elseif key:lower() == "name" then -- Set name. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword name = %s", tostring(val))) switch.name = val elseif key:lower() == "quiet" then -- Set quiet. -- LOGGING DISABLED WHEN COMPILING("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. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword modulations = %s", tostring(val))) switch.modulations = val elseif key:lower() == "path" then -- Set path. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword path = %s", tostring(val))) switch.path = val end end return switch end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Main event handler (used for PLAYER ENTER UNIT events) ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Event handler. veafRadio.eventHandler = {} --- Handle world events. function veafRadio.eventHandler:onEvent(Event) local EVENTS = { [0] = "S_EVENT_INVALID", [1] = "S_EVENT_SHOT", [2] = "S_EVENT_HIT", [3] = "S_EVENT_TAKEOFF", [4] = "S_EVENT_LAND", [5] = "S_EVENT_CRASH", [6] = "S_EVENT_EJECTION", [7] = "S_EVENT_REFUELING", [8] = "S_EVENT_DEAD", [9] = "S_EVENT_PILOT_DEAD", [10] = "S_EVENT_BASE_CAPTURED", [11] = "S_EVENT_MISSION_START", [12] = "S_EVENT_MISSION_END", [13] = "S_EVENT_TOOK_CONTROL", [14] = "S_EVENT_REFUELING_STOP", [15] = "S_EVENT_BIRTH", [16] = "S_EVENT_HUMAN_FAILURE", [17] = "S_EVENT_DETAILED_FAILURE", [18] = "S_EVENT_ENGINE_STARTUP", [19] = "S_EVENT_ENGINE_SHUTDOWN", [20] = "S_EVENT_PLAYER_ENTER_UNIT", [21] = "S_EVENT_PLAYER_LEAVE_UNIT", [22] = "S_EVENT_PLAYER_COMMENT", [23] = "S_EVENT_SHOOTING_START", [24] = "S_EVENT_SHOOTING_END", [25] = "S_EVENT_MARK_ADDED", [26] = "S_EVENT_MARK_CHANGE", [27] = "S_EVENT_MARK_REMOVED", [28] = "S_EVENT_KILL", [29] = "S_EVENT_SCORE", [30] = "S_EVENT_UNIT_LOST", [31] = "S_EVENT_LANDING_AFTER_EJECTION"} local enabledEvents = { ["S_EVENT_INVALID"] = false, ["S_EVENT_SHOT"] = false, ["S_EVENT_HIT"] = false, ["S_EVENT_TAKEOFF"] = false, ["S_EVENT_LAND"] = false, ["S_EVENT_CRASH"] = false, ["S_EVENT_EJECTION"] = false, ["S_EVENT_REFUELING"] = false, ["S_EVENT_DEAD"] = true, ["S_EVENT_PILOT_DEAD"] = true, ["S_EVENT_BASE_CAPTURED"] = false, ["S_EVENT_MISSION_START"] = false, ["S_EVENT_MISSION_END"] = false, ["S_EVENT_TOOK_CONTROL"] = true, ["S_EVENT_REFUELING_STOP"] = false, ["S_EVENT_BIRTH"] = true, ["S_EVENT_HUMAN_FAILURE"] = false, ["S_EVENT_DETAILED_FAILURE"] = false, ["S_EVENT_ENGINE_STARTUP"] = false, ["S_EVENT_ENGINE_SHUTDOWN"] = false, ["S_EVENT_PLAYER_ENTER_UNIT"] = true, ["S_EVENT_PLAYER_LEAVE_UNIT"] = true, ["S_EVENT_PLAYER_COMMENT"] = true, ["S_EVENT_SHOOTING_START"] = false, ["S_EVENT_SHOOTING_END"] = false, ["S_EVENT_MARK_ADDED"] = false, ["S_EVENT_MARK_CHANGE"] = false, ["S_EVENT_MARK_REMOVED"] = false, ["S_EVENT_KILL"] = false, ["S_EVENT_SCORE"] = false, ["S_EVENT_UNIT_LOST"] = true, ["S_EVENT_LANDING_AFTER_EJECTION"] = false } -- Only interested in S_EVENT_BIRTH (S_EVENT_PLAYER_ENTER_UNIT is not fired in MP) if Event == nil or not(enabledEvents[EVENTS[Event.id]]) then return true end -- Debug output. local _unitname = "" -- LOGGING DISABLED WHEN COMPILING(string.format("got event %s", veaf.p(EVENTS[Event.id]))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event id = %s", veaf.p(Event.id))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event time = %s", veaf.p(Event.time))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event idx = %s", veaf.p(Event.idx))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event coalition = %s", veaf.p(Event.coalition))) -- LOGGING DISABLED WHEN COMPILING(string.format("Event group id = %s", veaf.p(Event.groupID))) if Event.initiator ~= nil and Event.initiator.getName then _unitname = Event.initiator:getName() -- LOGGING DISABLED WHEN COMPILING(string.format("Event ini unit = %s", veaf.p(_unitname))) end -- LOGGING DISABLED WHEN COMPILING(string.format("Event text = \n%s", veaf.p(Event.text))) local refreshRadioMenu = false -- human unit birth if Event.id == world.event.S_EVENT_BIRTH then if _unitname and veafRadio.humanUnits[_unitname] then -- a human spawned in this slot ! Yay ! -- LOGGING DISABLED WHEN COMPILING(string.format("a human spawned in this slot: %s", veaf.p(_unitname))) veafRadio.humanUnits[_unitname].spawned = true -- refresh the radio menu refreshRadioMenu = true end end -- human unit death if Event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT then if _unitname and veafRadio.humanUnits[_unitname] then -- it was a human in this slot ! if veafRadio.humanUnits[_unitname] then veafRadio.humanUnits[_unitname].spawned = false end -- refresh the radio menu refreshRadioMenu = true end end if refreshRadioMenu then -- refresh the radio menu veafRadio.refreshRadioMenu() -- TODO refresh it only for this player ? Is this even possible ? -- LOGGING DISABLED WHEN COMPILING(string.format("refreshRadioMenu() following event %s of human unit %s", veaf.p(EVENTS[Event.id]), veaf.p(_unitname))) end end -- function veafRadio.eventHandler:onEvent(Event) -- local EVENTS = { -- [0] = "S_EVENT_INVALID", -- [1] = "S_EVENT_SHOT", -- [2] = "S_EVENT_HIT", -- [3] = "S_EVENT_TAKEOFF", -- [4] = "S_EVENT_LAND", -- [5] = "S_EVENT_CRASH", -- [6] = "S_EVENT_EJECTION", -- [7] = "S_EVENT_REFUELING", -- [8] = "S_EVENT_DEAD", -- [9] = "S_EVENT_PILOT_DEAD", -- [10] = "S_EVENT_BASE_CAPTURED", -- [11] = "S_EVENT_MISSION_START", -- [12] = "S_EVENT_MISSION_END", -- [13] = "S_EVENT_TOOK_CONTROL", -- [14] = "S_EVENT_REFUELING_STOP", -- [15] = "S_EVENT_BIRTH", -- [16] = "S_EVENT_HUMAN_FAILURE", -- [17] = "S_EVENT_DETAILED_FAILURE", -- [18] = "S_EVENT_ENGINE_STARTUP", -- [19] = "S_EVENT_ENGINE_SHUTDOWN", -- [20] = "S_EVENT_PLAYER_ENTER_UNIT", -- [21] = "S_EVENT_PLAYER_LEAVE_UNIT", -- [22] = "S_EVENT_PLAYER_COMMENT", -- [23] = "S_EVENT_SHOOTING_START", -- [24] = "S_EVENT_SHOOTING_END", -- [25] = "S_EVENT_MARK_ADDED", -- [26] = "S_EVENT_MARK_CHANGE", -- [27] = "S_EVENT_MARK_REMOVED", -- [28] = "S_EVENT_KILL", -- [29] = "S_EVENT_SCORE", -- [30] = "S_EVENT_UNIT_LOST", -- [31] = "S_EVENT_LANDING_AFTER_EJECTION"} -- local _unitname = "" -- veaf.loggers.get(veafRadio.Id):info("GOT AN EVENT") -- veaf.loggers.get(veafRadio.Id):info(string.format("Event id = %s - %s", tostring(Event.id), EVENTS[Event.id])) -- veaf.loggers.get(veafRadio.Id):info(string.format("Event time = %s", tostring(Event.time))) -- veaf.loggers.get(veafRadio.Id):info(string.format("Event idx = %s", tostring(Event.idx))) -- veaf.loggers.get(veafRadio.Id):info(string.format("Event coalition = %s", tostring(Event.coalition))) -- veaf.loggers.get(veafRadio.Id):info(string.format("Event group id = %s", tostring(Event.groupID))) -- if Event.initiator ~= nil then -- _unitname = Event.initiator:getName() -- veaf.loggers.get(veafRadio.Id):info(string.format("Event ini unit = %s", tostring(_unitname))) -- end -- veaf.loggers.get(veafRadio.Id):info(string.format("Event text = \n%s", tostring(Event.text))) -- if Event.id == 15 and _unitname and veafRadio.humanUnits[_unitname] then -- -- refresh the radio menu -- veafRadio.refreshRadioMenu() -- TODO refresh it only for this player ? Is this even possible ? -- -- debug with logInfo message to check if this mechanism is working -- veaf.loggers.get(veafRadio.Id):info(string.format("refreshRadioMenu() following event S_EVENT_BIRTH of human unit %s", tostring(_unitname))) -- end -- end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafRadio._proxyMethod(parameters) -- LOGGING DISABLED WHEN COMPILING("parameters="..veaf.p(parameters)) local realMethod, realParameters = veaf.safeUnpack(parameters) -- LOGGING DISABLED WHEN COMPILING("realMethod="..veaf.p(realMethod)) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("veafRadio._refreshRadioMenu()")) veafRadio.refreshRadioMenuDelayedScheduling = nil -- completely delete the dcs radio menu -- LOGGING DISABLED WHEN COMPILING("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 local radioMeasures = { nbMenus = 0 ,maxNbMenusInGroups = 0 ,nbCommands = 0 ,maxNbCommandsInGroups = 0 } veafRadio.radioMenuSize = {} veafRadio.addSizeForAll(string.len(veafRadio.RadioMenuName)) -- create all the commands and submenus in the dcs radio menu -- LOGGING DISABLED WHEN COMPILING("create all the commands and submenus in the dcs radio menu") veafRadio.refreshRadioSubmenu(nil, veafRadio.radioMenu, radioMeasures) -- warn if the size starts to get too big local maxSize = 0 local maxGroup = -1 for group, size in pairs(veafRadio.radioMenuSize) do if maxSize < size then maxSize = size maxGroup = group end if veafRadio.MAXIMUM_SIZE > 0 and size >= veafRadio.MAXIMUM_SIZE then veafRadio.reportRadioMenuSizeBreached("veafRadio._refreshRadioMenu()", group, size) end end -- LOGGING DISABLED WHEN COMPILING(string.format("veafRadio._refreshRadioMenu() max(veafRadio.radioMenuSize)=%d,%d",maxSize, maxGroup)) -- LOGGING DISABLED WHEN COMPILING("radioMeasures="..veaf.p(radioMeasures)) 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 -- LOGGING DISABLED WHEN COMPILING("adding secured command") _method = veafRadio._proxyMethod _parameters = {command.method, _parameters} if veafSecurity.isAuthenticated() then _title = "-" .. title else _title = "+" .. title end end -- LOGGING DISABLED WHEN COMPILING("_title=%s", veaf.p(_title)) -- LOGGING DISABLED WHEN COMPILING("_parameters=%s", veaf.p(_parameters)) if groupId then -- LOGGING DISABLED WHEN COMPILING("adding for group %s command %s",groupId or "", _title or "") missionCommands.addCommandForGroup(groupId, _title, menu, _method, _parameters) else -- LOGGING DISABLED WHEN COMPILING("adding for all command %s",_title or "") missionCommands.addCommand(_title, menu, _method, _parameters) end end function veafRadio.refreshRadioSubmenu(parentRadioMenu, radioMenu, radioMeasures) -- LOGGING DISABLED WHEN COMPILING("veafRadio.refreshRadioSubmenu %s", veaf.p(veaf.ifnn(radioMenu, "title"))) if not radioMenu or not radioMenu.title then return end local trace = false local measures_addMenu = function(group) radioMeasures.nbMenus = radioMeasures.nbMenus + 1 if group ~= nil then if radioMeasures.maxNbMenusInGroups < radioMeasures.nbMenus then radioMeasures.maxNbMenusInGroups = radioMeasures.nbMenus end end end local measures_addCommand = function(group) radioMeasures.nbCommands = radioMeasures.nbCommands + 1 if group ~= nil then if radioMeasures.maxNbCommandsInGroups < radioMeasures.nbCommands then radioMeasures.maxNbCommandsInGroups = radioMeasures.nbCommands end end end -- warn if the size starts to get too big for group, size in pairs(veafRadio.radioMenuSize) do if veafRadio.MAXIMUM_SIZE > 0 and size >= veafRadio.MAXIMUM_SIZE then veafRadio.reportRadioMenuSizeBreached(string.format("veafRadio.refreshRadioSubmenu()",radioMenu.title), group, size) end end -- create the radio menu in DCS veafRadio.addSizeForAll(string.len(radioMenu.title)) if parentRadioMenu then radioMenu.dcsRadioMenu = missionCommands.addSubMenu(radioMenu.title, parentRadioMenu.dcsRadioMenu) else radioMenu.dcsRadioMenu = missionCommands.addSubMenu(radioMenu.title) end measures_addMenu() -- create the commands in the radio menu for count = 1,#radioMenu.commands do local command = radioMenu.commands[count] -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("groupId=%s",veaf.p(groupId))) for _, callsign in pairs(groupData.callsigns) do -- LOGGING DISABLED WHEN COMPILING(string.format("callsign=%s",veaf.p(callsign))) local unitData = groupData.units[callsign] local unitName = unitData.name -- LOGGING DISABLED WHEN COMPILING(string.format("unitName=%s",veaf.p(unitName))) local humanUnit = veafRadio.humanUnits[unitName] -- LOGGING DISABLED WHEN COMPILING(string.format("humanUnit=%s",veaf.p(humanUnit))) -- LOGGING DISABLED WHEN COMPILING(string.format("checking if unit %s is spawned",veaf.p(unitName))) if humanUnit and humanUnit.spawned then -- LOGGING DISABLED WHEN COMPILING(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.addSizeForGroup(groupId, string.len(_title)) veafRadio._addCommand(groupId, _title, radioMenu.dcsRadioMenu, command, parameters) measures_addCommand(groupId) end alreadyDoneGroups[groupId] = true end end end else veafRadio.addSizeForAll(string.len(command.title)) veafRadio._addCommand(nil, command.title, radioMenu.dcsRadioMenu, command, command.parameters) measures_addCommand() end end -- recurse to create the submenus in the radio menu for count = 1,#radioMenu.subMenus do local subMenu = radioMenu.subMenus[count] veafRadio.refreshRadioSubmenu(radioMenu, subMenu, radioMeasures) 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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING("searching for " .. subMenu.title) local v = menu.subMenus[i] -- LOGGING DISABLED WHEN COMPILING("checking " .. v.title) if v == subMenu or v.title == subMenu then -- LOGGING DISABLED WHEN COMPILING("found ! removing " .. v.title) return false else -- LOGGING DISABLED WHEN COMPILING("keeping " .. v.title) return true end end); end -- build a paginated submenu (internal paginating method) local function _buildRadioMenuPage(menu, titles, elementsByTitle, addCommandToSubmenuMethod, pageSize, startIndex) -- LOGGING DISABLED WHEN COMPILING(string.format("_buildRadioMenuPage(pageSize=%s, startIndex=%s)",tostring(pageSize), tostring(startIndex))) local titlesCount = #titles -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("endIndex = %d",endIndex)) -- LOGGING DISABLED WHEN COMPILING(string.format("adding commands from %d to %d",startIndex, endIndex)) for index = startIndex, endIndex do local title = titles[index] -- LOGGING DISABLED WHEN COMPILING(string.format("titles[%d] = %s",index, title)) local element = elementsByTitle[title] addCommandToSubmenuMethod(menu, title, element) end if endIndex < titlesCount then -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafRadio.addPaginatedRadioMenu(title=%s)",title)) local firstPagePath = veafRadio.addSubMenu(title, radioMenu) veafRadio.addPaginatedRadioElements(firstPagePath, addCommandToSubmenuMethod, elements, titleAttribute, sortAttribute) return firstPagePath end -- prepare humans units function veafRadio.buildHumanUnits() veafRadio.humanUnits = {} -- build menu for each player for name, unit in pairs(mist.DBs.humansByName) do -- not already in units list ? if veafRadio.humanUnits[unit.unitName] == nil then -- LOGGING DISABLED WHEN COMPILING(string.format("human player found name=%s, unitName=%s, groupId=%s", name, unit.unitName,unit.groupId)) local callsign = unit.callsign if type(callsign) == "table" then callsign = callsign["name"] end if type(callsign) == "number" then callsign = "" .. callsign end local unitObject = {name=unit.unitName, groupId=unit.groupId, callsign=callsign} veafRadio.humanUnits[unit.unitName] = unitObject -- LOGGING DISABLED WHEN COMPILING(string.format("veafRadio.humanUnits[%s]=\n%s",unit.unitName,veaf.p(veafRadio.humanUnits[unit.unitName]))) if not veafRadio.humanGroups[unit.groupId] then veafRadio.humanGroups[unit.groupId] = {} veafRadio.humanGroups[unit.groupId].callsigns = {} veafRadio.humanGroups[unit.groupId].units = {} end table.insert(veafRadio.humanGroups[unit.groupId].callsigns,callsign) veafRadio.humanGroups[unit.groupId].units[callsign] = unitObject end end -- sort callsigns for each group for _, groupData in pairs(veafRadio.humanGroups) do table.sort(groupData.callsigns) end end function veafRadio.addSizeForGroup(groupId, sizeToAdd) if not veafRadio.radioMenuSize then veafRadio.radioMenuSize = {} end if not veafRadio.radioMenuSize[groupId] then veafRadio.radioMenuSize[groupId] = 0 end veafRadio.radioMenuSize[groupId] = veafRadio.radioMenuSize[groupId] + sizeToAdd end function veafRadio.addSizeForAll(sizeToAdd) for groupId, _ in pairs(veafRadio.humanGroups) do veafRadio.addSizeForGroup(groupId, sizeToAdd) end end function veafRadio.reportRadioMenuSizeBreached_reset() veafRadio.reportRadioMenuSizeBreached_ALREADYDONE = false end function veafRadio.reportRadioMenuSizeBreached(text, group, size) if not veafRadio.reportRadioMenuSizeBreached_ALREADYDONE then local message = string.format("%s - Maximum radio menu size reached : [%s]%d / %d",text or "", tostring(group), size, veafRadio.MAXIMUM_SIZE) veaf.loggers.get(veafRadio.Id):warn(string.format("%s - Maximum radio menu size reached : [%s]%d / %d",text or "", tostring(group), size, veafRadio.MAXIMUM_SIZE)) trigger.action.outText(string.format("Maximum radio menu size reached : [%s]%d / %d",tostring(group), size, veafRadio.MAXIMUM_SIZE),5) veafRadio.reportRadioMenuSizeBreached_ALREADYDONE = true mist.scheduleFunction(veafRadio.reportRadioMenuSizeBreached_reset,{},timer.getTime()+60) end end function veafRadio.getHumanUnitOrWingman(unitName) local result = Unit.getByName(unitName) if not result then local unitData = veafRadio.humanUnits[unitName] -- LOGGING DISABLED WHEN COMPILING(string.format("unitData=%s",veaf.p(unitData))) if unitData and unitData.groupId then local mistGroup = mist.DBs.groupsById[unitData.groupId] -- LOGGING DISABLED WHEN COMPILING(string.format("mistGroup=%s",veaf.p(mistGroup))) if mistGroup then local group = Group.getByName(mistGroup.groupName) if group then -- LOGGING DISABLED WHEN COMPILING(string.format("group=%s",veaf.p(group))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("result=%s",veaf.p(result))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(string.format("adding beacon %s", tostring(name))) veafRadio.beacons[name:lower()] = beacon end function veafRadio._runBeacons() -- LOGGING DISABLED WHEN COMPILING("_runBeacons()") local now = timer.getTime() -- LOGGING DISABLED WHEN COMPILING(string.format("now = %s", tostring(now))) for name, beacon in pairs(veafRadio.beacons) do -- LOGGING DISABLED WHEN COMPILING(string.format("checking %s supposed to run at %s", tostring(beacon.name), tostring(beacon.nextRun))) if beacon.nextRun <= now then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("veafRadio.createUserMenu(groupId=%s, configuration=%s)",veaf.p(groupId), veaf.p(configuration)) local function _recursivelyCreateMenu(configuration, parentMenu) -- LOGGING DISABLED WHEN COMPILING("_recursivelyCreateMenu(configuration=%s, parentMenu=%s)",veaf.p(configuration), veaf.p(parentMenu)) local result for _, item in pairs(configuration) do local itemType = item[1] -- LOGGING DISABLED WHEN COMPILING("itemType = [%s]",veaf.p(itemType)) local name = item[2] -- LOGGING DISABLED WHEN COMPILING("name = [%s]",veaf.p(name)) if itemType == "menu" then -- this is a menu with a content local content = item[3] -- LOGGING DISABLED WHEN COMPILING("content = [%s]",veaf.p(content)) -- LOGGING DISABLED WHEN COMPILING("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] -- LOGGING DISABLED WHEN COMPILING("aFunction = [%s]",veaf.p(aFunction)) local parameters = item[4] -- LOGGING DISABLED WHEN COMPILING("parameters = [%s]",veaf.p(parameters)) -- LOGGING DISABLED WHEN COMPILING("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" -- LOGGING DISABLED WHEN COMPILING(string.format("srsConfigPath = %s", tostring(srsConfigPath))) --local test = l_lfs.currentdir() -- LOGGING DISABLED WHEN COMPILING(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" -- LOGGING DISABLED WHEN COMPILING(string.format("STTS.SRS_PORT = %s", tostring(STTS.SRS_PORT))) -- LOGGING DISABLED WHEN COMPILING(string.format("STTS.DIRECTORY = %s", tostring(STTS.DIRECTORY))) -- LOGGING DISABLED WHEN COMPILING(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.buildHumanUnits() veafRadio.refreshRadioMenu(false) mist.scheduleFunction(veafRadio._refreshRadioMenu,{},timer.getTime()+15) -- Add "player unit birth" event handler. world.addEventHandler(veafRadio.eventHandler) -- add marker change event handler veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafRadio.onEventMarkChange) -- 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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("payload = %s", veaf.p(payload))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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)) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafRemote.executeCommand(eventPos, eventText) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 "" -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("running veafCombatMission.executeCommandFromRemote")) _status, _retval = pcall(veafCombatMission.executeCommandFromRemote, _parameters) elseif _module == "point" then -- LOGGING DISABLED WHEN COMPILING(string.format("running veafNamedPoints.executeCommandFromRemote")) _status, _retval = pcall(veafNamedPoints.executeCommandFromRemote, _parameters) elseif _module == "atis" or _module == "atc" or _module == "weather" then -- LOGGING DISABLED WHEN COMPILING(string.format("running veafWeather.executeCommandFromRemote")) _status, _retval = pcall(veafWeather.executeCommandFromRemote, _parameters) elseif _module == "alias" then -- LOGGING DISABLED WHEN COMPILING(string.format("running veafShortcuts.executeCommandFromRemote")) _status, _retval = pcall(veafShortcuts.executeCommandFromRemote, _parameters) elseif _module == "carrier" then -- LOGGING DISABLED WHEN COMPILING(string.format("running veafShortcuts.executeCommandFromRemote")) _status, _retval = pcall(veafCarrierOperations.executeCommandFromRemote, _parameters) elseif _module == "secu" then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("_status = [%s]",veaf.p(_status))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafRemote.getRemoteUser([%s])",veaf.p(username))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafRemote.getRemoteUserFromUnit([%s])",veaf.p(unitName))) -- LOGGING DISABLED WHEN COMPILING(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.57.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" --- Name of the spawned units group veafSpawn.RedSpawnedUnitsGroupName = "VEAF Spawned Units" --- 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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafSpawn.executeCommand(eventText=[%s])", eventText)) -- LOGGING DISABLED WHEN COMPILING(string.format("coalition=%s", veaf.p(coalition))) -- LOGGING DISABLED WHEN COMPILING(string.format("markId=%s", veaf.p(markId))) -- LOGGING DISABLED WHEN COMPILING(string.format("bypassSecurity=%s", veaf.p(bypassSecurity))) -- LOGGING DISABLED WHEN COMPILING(string.format("repeatCount=%s", veaf.p(repeatCount))) -- LOGGING DISABLED WHEN COMPILING(string.format("repeatDelay=%s", veaf.p(repeatDelay))) -- LOGGING DISABLED WHEN COMPILING(string.format("route=%s", veaf.p(route))) -- LOGGING DISABLED WHEN COMPILING(string.format("allowStartDelay=%s", veaf.p(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("options.side=%s",tostring(options.side))) -- LOGGING DISABLED WHEN COMPILING(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.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.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.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.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.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.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.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.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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("laserB=%s", tostring(laserB))) -- LOGGING DISABLED WHEN COMPILING(string.format("laserCD=%s", tostring(laserCD))) -- LOGGING DISABLED WHEN COMPILING(string.format("frequency=%s", tostring(frequency))) return frequency else return nil end end --- Extract keywords from mark text. function veafSpawn.markTextAnalysis(text) -- LOGGING DISABLED WHEN COMPILING(string.format("veafSpawn.markTextAnalysis(text=%s)", text)) -- Option parameters extracted from the mark text. local options = {} 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. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword unitname = %s", tostring(val))) options.unitName = val end if key:lower() == "name" then -- Set name. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword name = %s", tostring(val))) options.name = val end if (key:lower() == "destination" or key:lower() == "dest") then -- Set destination. -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("Keyword isconvoy found") options.convoy = true end if key:lower() == "patrol" then -- LOGGING DISABLED WHEN COMPILING("Keyword patrol found") options.patrol = true end if key:lower() == "offroad" then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword pointdefense found")) options.pointDefense = true if val ~= "" then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword radius = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.radius = nVal end if key:lower() == "spacing" then -- Set spacing. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword spacing = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.spacing = nVal end if key:lower() == "multiplier" then -- Set multiplier. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword multiplier = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.multiplier = nVal end if key:lower() == "alt" then -- Set altitude. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword alt = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.altitude = nVal end if key:lower() == "altdelta" then -- Set altitude delta. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword altdelta = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.altitudedelta = nVal end if key:lower() == "speed" then -- Set speed. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword speed = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.speed = nVal end if key:lower() == "capradius" then -- Set capradius. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword capradius = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.capradius = nVal end if key:lower() == "shells" then -- Set altitude. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword shells = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.shells = nVal end if key:lower() == "hdg" then -- Set heading. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword hdg = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.heading = nVal end if key:lower() == "heading" then -- Set heading. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword heading = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.heading = nVal end if key:lower() == "country" then -- Set country -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword country = %s", tostring(val))) options.country = val:upper() end if key:lower() == "side" then -- Set side -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword password", tostring(val))) options.password = val end if key:lower() == "power" then -- Set bomb power. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword power = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.power = nVal end if key:lower() == "laser" then -- Set laser code. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(string.format("freq = %s", tostring(val))) options.freq = val end if key:lower() == "mod" then -- Set JTAC/AFAC modulation. -- LOGGING DISABLED WHEN COMPILING(string.format("mod = %s", tostring(val))) options.mod = val end if key:lower() == "band" then -- Set TACAN band -- LOGGING DISABLED WHEN COMPILING(string.format("band = %s", tostring(val))) options.tacanBand = val end if key:lower() == "code" then -- Set TACAN code -- LOGGING DISABLED WHEN COMPILING(string.format("code = %s", tostring(val))) options.tacanCode = val end if key:lower() == "channel" then -- Set TACAN channel. -- LOGGING DISABLED WHEN COMPILING(string.format("channel = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.tacanChannel = nVal end if key:lower() == "arrow" then -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword arrow = %s", tostring(val))) options.drawArrow = true end if key:lower() == "fill" then -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword fill = %s", tostring(val))) options.drawFillColor = val end if key:lower() == "color" then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword skill = %s", tostring(val))) options.skill = val end if key:lower() == "dist" or key:lower() == "distance" then -- Set distance. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword name = %s", tostring(val))) options.cargoType = val end if options.cargo and key:lower() == "weight" then -- Set cargo type. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("Keyword noFarpMarkers is set") options.noFarpMarkers = true end if options.cargo and key:lower() == "smoke" then -- Mark with green smoke. -- LOGGING DISABLED WHEN COMPILING("Keyword smoke is set") options.cargoSmoke = true end if key:lower() == "size" then -- Set size. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword size = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.size = nVal end if key:lower() == "defense" then -- Set defense. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword repeat = %s", tostring(val))) local nVal = veaf.getRandomizableNumeric(val) options.repeatCount = nVal end if key:lower() == "delay" then -- Set delay. -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword static found")) options.forceStatic = true end if key:lower() == "immortal" then -- Set spawned unit to invisible and immortal -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword immortal found")) options.immortal = true end if key:lower() == "delayed" then -- Set delayed start on first spawn occurence -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword showmfd found")) options.showMFD = true end if key:lower() == "disperse" then -- Set hiddenOnMFD option or not -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD, shuffle) -- LOGGING DISABLED WHEN COMPILING(string.format("doSpawnGroup(country=%s, alt=%s, hdg=%s, spacing=%s, groupName=%s, silent=%s, hasDest=%s, hiddenOnMFD=%s, shuffle=%s)", veaf.p(country), veaf.p(alt), veaf.p(hdg), veaf.p(spacing), veaf.p(groupName), veaf.p(silent), veaf.p(hasDest), veaf.p(hiddenOnMFD), veaf.p(shuffle))) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 .. " #" .. veafSpawn.spawnedUnitsCounter end 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 } -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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)) -- LOGGING DISABLED WHEN COMPILING("spawnPosition=%s", veaf.p(spawnPosition)) if not name or name == "" then local _lat, _lon = coord.LOtoLL(spawnSpot) -- LOGGING DISABLED WHEN COMPILING("_lat=%s", veaf.p(_lat)) -- LOGGING DISABLED WHEN COMPILING("_lon=%s", veaf.p(_lon)) local _mgrs = coord.LLtoMGRS(_lat, _lon) -- LOGGING DISABLED WHEN COMPILING("_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, ["dynamicSpawn"] = true } mist.dynAddStatic(_farpStatic) local _spawnedFARP = StaticObject.getByName(name) -- LOGGING DISABLED WHEN COMPILING("_spawnedFARP=%s", veaf.p(_spawnedFARP)) if _spawnedFARP then -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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)) -- LOGGING DISABLED WHEN COMPILING("spawnPosition=%s", veaf.p(_spawnPosition)) if not _fobName or _fobName == "" then local _lat, _lon = coord.LOtoLL(spawnSpot) -- LOGGING DISABLED WHEN COMPILING("_lat=%s", veaf.p(_lat)) -- LOGGING DISABLED WHEN COMPILING("_lon=%s", veaf.p(_lon)) local _mgrs = coord.LLtoMGRS(_lat, _lon) -- LOGGING DISABLED WHEN COMPILING("_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) -- LOGGING DISABLED WHEN COMPILING("_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" -- LOGGING DISABLED WHEN COMPILING("_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, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD) -- LOGGING DISABLED WHEN COMPILING("spawnGroup(name=%s, country=%s, alt=%s, hdg=%s, spacing=%s, groupName=%s, silent=%s, hiddenOnMFD=%s)", veaf.p(name), veaf.p(country), veaf.p(alt), veaf.p(hdg), veaf.p(spacing), veaf.p(silent), veaf.p(groupName), veaf.p(hiddenOnMFD)) local spawnedGroupName = veafSpawn.doSpawnGroup(spawnSpot, radius, name, country, alt, hdg, spacing, groupName, silent, hasDest, hiddenOnMFD) return spawnedGroupName end function veafSpawn._createDcsUnits(country, units, groupName, hiddenOnMFD, hasDest) -- LOGGING DISABLED WHEN COMPILING(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 unitName = groupName .. " / " .. unit.displayName .. " #" .. i local spawnPosition = unit.spawnPoint local hdg = spawnPosition.hdg or math.random(0, 359) -- 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"] = "Excellent", ["heading"] = hdg } -- LOGGING DISABLED WHEN COMPILING(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, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnInfantryGroup(country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", veaf.p(country), veaf.p(side), veaf.p(heading), veaf.p(spacing), veaf.p(defense), veaf.p(armor), veaf.p(size), veaf.p(silent), veaf.p(hiddenOnMFD))) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING("spawnSpot=" .. veaf.vecToString(spawnSpot)) local groupName = "spawn-" .. math.random(99999) .. " - Infantry Section " local group = veafCasMission.generateInfantryGroup(groupName, defense, armor, side, size) local group = veafUnits.processGroup(group) local groupPosition = veaf.placePointOnLand(spawnSpot) -- LOGGING DISABLED WHEN COMPILING(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, country, side, heading, spacing, defense, armor, size, silent, hasDest, hiddenOnMFD) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnArmoredPlatoon(country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hasDest=%s, hiddenOnMFD=%s)", veaf.p(country), veaf.p(side), veaf.p(heading), veaf.p(spacing), veaf.p(defense), veaf.p(armor), veaf.p(size), veaf.p(silent), veaf.p(hasDest), veaf.p(hiddenOnMFD))) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING("spawnSpot=" .. veaf.vecToString(spawnSpot)) local groupName = "spawn-" .. math.random(99999) .. " - Armored Platoon " local group = veafCasMission.generateArmorPlatoon(groupName, defense, armor, side, size) local group = veafUnits.processGroup(group) local groupPosition = veaf.placePointOnLand(spawnSpot) -- LOGGING DISABLED WHEN COMPILING(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 armored platoon "..groupName, 5) end return groupName end --- Spawns a dynamic air defense battery function veafSpawn.spawnAirDefenseBattery(spawnSpot, radius, country, side, heading, spacing, defense, silent, hasDest, hiddenOnMFD) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnAirDefenseBattery(country=%s, side=%s, heading=%s, spacing=%s, defense=%s, silent=%s, hasDest=%s, hiddenOnMFD=%s)", veaf.p(country), veaf.p(side), veaf.p(heading), veaf.p(spacing), veaf.p(defense), veaf.p(silent), veaf.p(hasDest), veaf.p(hiddenOnMFD))) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING("spawnSpot=" .. veaf.vecToString(spawnSpot)) local groupName = "spawn-" .. math.random(99999) .. " - Air Defense Battery " local group = veafCasMission.generateAirDefenseGroup(groupName, defense, side) local group = veafUnits.processGroup(group) local groupPosition = veaf.placePointOnLand(spawnSpot) -- LOGGING DISABLED WHEN COMPILING(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, country, side, heading, spacing, defense, size, silent, hasDest, hiddenOnMFD) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnTransportCompany(country=%s, side=%s, heading=%s, spacing=%s, defense=%s, size=%s, silent=%s, hasDest=%s, hiddenOnMFD=%s)", veaf.p(country), veaf.p(side), veaf.p(heading), veaf.p(spacing), veaf.p(defense), veaf.p(size), veaf.p(silent), veaf.p(hasDest), veaf.p(hiddenOnMFD))) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING("spawnSpot=" .. veaf.vecToString(spawnSpot)) local groupName = "spawn-" .. math.random(99999) .. " - Transport Company " local group = veafCasMission.generateTransportCompany(groupName, defense, side, size) local group = veafUnits.processGroup(group) local groupPosition = veaf.placePointOnLand(spawnSpot) -- LOGGING DISABLED WHEN COMPILING(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, country, side, heading, spacing, defense, armor, size, silent, hiddenOnMFD) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnFullCombatGroup(country=%s, side=%s, heading=%s, spacing=%s, defense=%s, armor=%s, size=%s, silent=%s, hiddenOnMFD=%s)", veaf.p(country), veaf.p(side), veaf.p(heading), veaf.p(spacing), veaf.p(defense), veaf.p(armor), veaf.p(size), veaf.p(silent), veaf.p(hiddenOnMFD))) local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING("spawnSpot=" .. veaf.vecToString(spawnSpot)) local groupName = "spawn-" .. math.random(99999) .. " - Full Combat Group " 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, radius, country, side, heading, spacing, speed, patrol, offroad, destination, defense, size, armor, silent, hiddenOnMFD) -- LOGGING DISABLED WHEN COMPILING(string.format("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])", veaf.p(spawnSpot), veaf.p(name), veaf.p(radius), veaf.p(country), veaf.p(side), veaf.p(speed), veaf.p(patrol), veaf.p(offroad), veaf.p(destination), veaf.p(defense), veaf.p(size), veaf.p(armor), veaf.p(silent), veaf.p(hiddenOnMFD))) if not(destination) then trigger.action.outText("No destination enterred !", 5) return false end local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("_lat=%s",veaf.p(_lat))) -- LOGGING DISABLED WHEN COMPILING(string.format("_lon=%s",veaf.p(_lon))) if _lat and _lon then point = coord.LLtoLO(_lat, _lon) -- LOGGING DISABLED WHEN COMPILING(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 = "convoy-" .. groupId 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 -- LOGGING DISABLED WHEN COMPILING("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, country, alt, hdg, unitName, role, static, code, freq, mod, silent, hiddenOnMFD) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnUnit(name = %s, country=%s, alt=%d, hdg=%d, unitName=%s, role=%s, static=%s, code=%s, freq=%s, mod=%s, silent=%s, hiddenOnMFD=%s)", veaf.p(name), veaf.p(country), veaf.p(alt), veaf.p(hdg), veaf.p(unitName), veaf.p(role), veaf.p(static), veaf.p(code), veaf.p(freq), veaf.p(mod), veaf.p(silent), veaf.p(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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("name=%s", tostring(name))) groupName = name unitName = name elseif role == "tacan" then local name = "TACAN " .. tostring(freq)..tostring(mod) -- LOGGING DISABLED WHEN COMPILING(string.format("name=%s", tostring(name))) groupName = name unitName = name else groupName = veafSpawn.RedSpawnedUnitsGroupName .. " #" .. veafSpawn.spawnedUnitsCounter if not unitName then unitName = unit.displayName .. " #" .. veafSpawn.spawnedUnitsCounter end end -- LOGGING DISABLED WHEN COMPILING("groupName="..groupName) -- LOGGING DISABLED WHEN COMPILING("unitName="..unitName) local spawnSpot = nil local nbTries = 25 repeat spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnPosition, radius)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Spawning AIRPLANE") mist.dynAdd({country = country, category = "PLANE", groupName = groupName, units = units, hiddenOnMFD = hiddenOnMFD}) elseif unit.naval then -- LOGGING DISABLED WHEN COMPILING("Spawning SHIP") mist.dynAdd({country = country, category = "SHIP", groupName = groupName, units = units, hiddenOnMFD = hiddenOnMFD}) else -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(string.format("name=%s", tostring(name))) -- LOGGING DISABLED WHEN COMPILING(string.format("freq=%s", tostring(freq))) local mod = string.upper(mod) or "X" -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("txFreq=%s", tostring(txFreq))) -- LOGGING DISABLED WHEN COMPILING(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, } } -- LOGGING DISABLED WHEN COMPILING(string.format("setting %s", veaf.p(command))) local spawnedGroup = Group.getByName(groupName) local controller = spawnedGroup:getController() controller:setCommand(command) -- LOGGING DISABLED WHEN COMPILING(string.format("done setting command")) end -- message the unit spawning -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("spawnCargo(cargoType = " .. cargoType ..")") local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("spawnLogistic()") local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("spawnCargo(cargoType = " .. cargoType ..")") local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("cargo defaultMass=%s", veaf.p(cargoWeight))) local minMass = cargoWeight + weightBiasMin * cargoWeight / (2*cargoWeightBiasScaleMax) local maxMass = cargoWeight + weightBiasMax * cargoWeight / (2*cargoWeightBiasScaleMax) -- LOGGING DISABLED WHEN COMPILING(string.format("cargo minMass=%s, cargo maxMass=%s", veaf.p(minMass), veaf.p(maxMass))) cargoWeight = math.random(minMass,maxMass) end if cargoWeight then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("doSpawnStatic(staticCategory = " .. staticCategory ..")") -- LOGGING DISABLED WHEN COMPILING("doSpawnStatic(staticType = " .. staticType ..")") local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("spawnBomb(power=" .. power ..")") local shellTime = 0 local shellDelay = 0 for shell=1,shells do local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING("spawnSpot=%s", spawnSpot) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("spawnSmoke(color=%s",veaf.p(color)) local radius = radius or 50 local shells = shells or 1 -- LOGGING DISABLED WHEN COMPILING("radius=%s", veaf.p(radius)) -- LOGGING DISABLED WHEN COMPILING("shells=%s", veaf.p(shells)) local shellTime = 0 for shell=1,shells do local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnSpot=%s", veaf.vecToString(spawnSpot))) local shellDelay = veafSpawn.ShellingInterval * (math.random(100) + 30)/100 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("spawnSignalFlare(color = " .. color ..")") local shellTime = 0 for shell=1,shells do local spawnSpot = veaf.placePointOnLand(mist.getRandPointInCircle(spawnSpot, radius)) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnSpot=%s", veaf.vecToString(spawnSpot))) local shellDelay = veafSpawn.ShellingInterval * (math.random(100) + 30)/100 local azimuth = math.random(359) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("spawnIlluminationFlare()") -- LOGGING DISABLED WHEN COMPILING("spawnSpot=%s", veaf.p(spawnSpot)) -- LOGGING DISABLED WHEN COMPILING("radius=%s", veaf.p(radius)) -- LOGGING DISABLED WHEN COMPILING("steps=%s", veaf.p(steps)) -- LOGGING DISABLED WHEN COMPILING("power=%s", veaf.p(power)) -- LOGGING DISABLED WHEN COMPILING("height=%s", veaf.p(height)) -- LOGGING DISABLED WHEN COMPILING("heading=%s", veaf.p(heading)) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafSpawn.destroyObjectWithFlak(%s, %s, %s)", veaf.p(power), veaf.p(power), veaf.p(density))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) } -- LOGGING DISABLED WHEN COMPILING(string.format("flakPoint=%s", veaf.p(flakPoint))) trigger.action.explosion(flakPoint, _power) end -- reschedule to check if the object is destroyed -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("destroy(radius=%s, unitName=%s)", tostring(radius), tostring(unitName))) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnSpot=%s", veaf.p(spawnSpot))) if unitName then -- destroy a specific unit local c = Unit.getByName(unitName) if c then -- LOGGING DISABLED WHEN COMPILING("destroy a specific unit") Unit.destroy(c) end -- or a specific static c = StaticObject.getByName(unitName) if c then -- LOGGING DISABLED WHEN COMPILING("destroy a specific static") StaticObject.destroy(c) end -- or a specific group c = Group.getByName(unitName) if c then -- LOGGING DISABLED WHEN COMPILING("destroy a specific group") Group.destroy(c) end else -- radius based destruction -- LOGGING DISABLED WHEN COMPILING("radius based destruction") local units = veaf.findUnitsInCircle(spawnSpot, radius or 150, true) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("distanceFromPlayer = %d",distanceFromPlayer)) if distanceFromPlayer < minDistance then minDistance = distanceFromPlayer closestConvoyName = name -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("veafSpawn.JTACAutoLase()") -- LOGGING DISABLED WHEN COMPILING(string.format("groupName=%s",tostring(groupName))) -- LOGGING DISABLED WHEN COMPILING(string.format("laserCode=%s",tostring(laserCode))) -- LOGGING DISABLED WHEN COMPILING(string.format("radioData=%s\n",veaf.p(radioData))) local _radio = radioData or {} -- LOGGING DISABLED WHEN COMPILING(string.format("_radio=%s\n",veaf.p(_radio))) -- LOGGING DISABLED WHEN COMPILING(string.format("calling CTLD")) ctld.JTACAutoLase(groupName, laserCode, false, "all", nil, _radio) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("veafSpawn.initializeAirUnitTemplates()") -- find groups with the air units template prefix -- LOGGING DISABLED WHEN COMPILING("find groups with the air units template prefix") local _prefix = veafSpawn.AirUnitTemplatesPrefix:upper() -- LOGGING DISABLED WHEN COMPILING("_prefix=%s",_prefix) local _templateGroups = {} local _groups = veaf.getGroupsOfCoalition() for _, group in pairs(_groups) do local _name = group:getName():upper() -- LOGGING DISABLED WHEN COMPILING("_name=%s",_name) if string.sub(_name,1,string.len(_prefix)) == _prefix then table.insert(_templateGroups, group) end end -- LOGGING DISABLED WHEN COMPILING("_templateGroups=%s", _templateGroups) for _, group in pairs(_templateGroups) do local _groupName = group:getName() -- LOGGING DISABLED WHEN COMPILING("_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 -- LOGGING DISABLED WHEN COMPILING("find groups within the veafSpawn.SpawnablePlanes table") for _, groupData in pairs(veafSpawn.SpawnablePlanes) do local _groupName = groupData.name -- LOGGING DISABLED WHEN COMPILING("_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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("veafSpawn.dumpSpawnablePlanesList(export_path=%s)", export_path) local jsonify = function(key, value) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("newGroupName=%s",newGroupName) -- LOGGING DISABLED WHEN COMPILING("AFAC_num=%s",AFAC_num) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("spawnSpot=%s", veaf.p(spawnSpot)) -- LOGGING DISABLED WHEN COMPILING("name=%s", veaf.p(name)) -- LOGGING DISABLED WHEN COMPILING("country=%s", veaf.p(country)) -- LOGGING DISABLED WHEN COMPILING("altitude (m)=%s", veaf.p(altitude)) -- LOGGING DISABLED WHEN COMPILING("speed (m/s)=%s", veaf.p(speed)) -- LOGGING DISABLED WHEN COMPILING("frequency=%s", veaf.p(frequency)) -- LOGGING DISABLED WHEN COMPILING("dcsFrequency=%s", veaf.p(dcsFrequency)) -- LOGGING DISABLED WHEN COMPILING("code=%s", veaf.p(code)) -- LOGGING DISABLED WHEN COMPILING("mod=%s", veaf.p(mod)) -- LOGGING DISABLED WHEN COMPILING("silent=%s", veaf.p(silent)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(veafSpawn.traceMarkerId, "AFAC", "teleportPoint", WP.one) -- LOGGING DISABLED WHEN COMPILING(veafSpawn.traceMarkerId, "AFAC", "setupPoint", WP.two) -- LOGGING DISABLED WHEN COMPILING(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" -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("unitName=%s",unitName) unit.unitName = unitName unit.name = unitName newGroup.sameName = true unit.alt = teleportSpot.alt -- LOGGING DISABLED WHEN COMPILING("newGroup=%s", veaf.p(newGroup, nil, {"route", "payload"})) local _spawnedGroup = mist.dynAdd(newGroup) if _spawnedGroup then -- LOGGING DISABLED WHEN COMPILING("_spawnedGroup=%s", veaf.p(_spawnedGroup, nil, {"route", "payload"})) -- LOGGING DISABLED WHEN COMPILING("_spawnedGroup.name=%s",_spawnedGroup.name) --mist.goRoute(_spawnedGroup.name, newRoute) _spawnedGroup.category = "AIRPLANE" _spawnedGroup.country = country -- LOGGING DISABLED WHEN COMPILING("_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 -- LOGGING DISABLED WHEN COMPILING("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))) -- LOGGING DISABLED WHEN COMPILING(string.format("markName=%s", veaf.p(markName))) if veafNamedPoints and markName then local existingPoint = veafNamedPoints.getPoint(markName) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("nameUpper=%s", veaf.p(nameUpper)) local templatesNamesToChooseFrom = {} local chosenTemplateName = nil for templateNameUpper, templateData in pairs(veafSpawn.airUnitTemplates) do -- LOGGING DISABLED WHEN COMPILING("templateNameUpper=%s", veaf.p(templateNameUpper)) if templateNameUpper:match(regexNameUpper) or templateNameUpper:match(escapedNameUpper) then local templateName = templateData.name -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("templatesNamesToChooseFrom=%s", veaf.p(templatesNamesToChooseFrom)) local chosenTemplateData = veaf.getGroupData(chosenTemplateName) -- LOGGING DISABLED WHEN COMPILING("found template=%s",chosenTemplateData) return chosenTemplateName, chosenTemplateData end function veafSpawn.spawnCombatAirPatrol(spawnSpot, radius, name, country, altitude, altitudeDelta, hdg, distance, speed, capRadius, skill, silent, hiddenOnMFD) -- LOGGING DISABLED WHEN COMPILING("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" -- LOGGING DISABLED WHEN COMPILING("spawnSpot=%s", veaf.p(spawnSpot)) -- LOGGING DISABLED WHEN COMPILING("radius=%s", veaf.p(radius)) -- LOGGING DISABLED WHEN COMPILING("name=%s", veaf.p(name)) -- LOGGING DISABLED WHEN COMPILING("country=%s", veaf.p(country)) -- LOGGING DISABLED WHEN COMPILING("altitude=%s", veaf.p(altitude)) -- LOGGING DISABLED WHEN COMPILING("altdelta=%s", veaf.p(altitudeDelta)) -- LOGGING DISABLED WHEN COMPILING("hdg=%s", veaf.p(hdg)) -- LOGGING DISABLED WHEN COMPILING("distance=%s", veaf.p(distance)) -- LOGGING DISABLED WHEN COMPILING("speed0=%s", veaf.p(speed0)) -- LOGGING DISABLED WHEN COMPILING("speed1=%s", veaf.p(speed1)) -- LOGGING DISABLED WHEN COMPILING("speed2=%s", veaf.p(speed2)) -- LOGGING DISABLED WHEN COMPILING("speed3=%s", veaf.p(speed3)) -- LOGGING DISABLED WHEN COMPILING("capRadius=%s", veaf.p(capRadius)) -- LOGGING DISABLED WHEN COMPILING("skill=%s", veaf.p(skill)) -- LOGGING DISABLED WHEN COMPILING("silent=%s", veaf.p(silent)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("final spawn, position=%s",position) -- get the template first waypoint's options -- LOGGING DISABLED WHEN COMPILING("chosenTemplateData=%s", veaf.p(chosenTemplateData)) local chosenTemplateWp1Task = {} if chosenTemplateData then local _route = chosenTemplateData.route -- LOGGING DISABLED WHEN COMPILING("_route=%s", veaf.p(_route)) if _route then local _points = _route.points -- LOGGING DISABLED WHEN COMPILING("_points=%s", veaf.p(_points)) if _points then local _point1 = _points[1] -- LOGGING DISABLED WHEN COMPILING("_point1=%s", veaf.p(_point1)) if _point1 then local _task = _point1.task -- LOGGING DISABLED WHEN COMPILING("_task=%s", veaf.p(_task)) if _task and "ComboTask" == _task.id then local _params = _task.params -- LOGGING DISABLED WHEN COMPILING("_params=%s", veaf.p(_params)) if _params then local _tasks = _params.tasks -- LOGGING DISABLED WHEN COMPILING("_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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("to create route, parameters=%s",parameters) local newRoute = getRoute(parameters) -- LOGGING DISABLED WHEN COMPILING(veafSpawn.traceMarkerId, "CAP", "wp1", parameters.wp1) -- LOGGING DISABLED WHEN COMPILING(veafSpawn.traceMarkerId, "CAP", "wp2", parameters.wp2) -- LOGGING DISABLED WHEN COMPILING(veafSpawn.traceMarkerId, "CAP", "wp3", parameters.wp3) -- LOGGING DISABLED WHEN COMPILING(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]) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("indexed spawnedUnitName=%s",spawnedUnitName) end -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("after mist.dynAdd, _spawnedGroup.name=%s",_spawnedGroup.name) -- LOGGING DISABLED WHEN COMPILING("after mist.dynAdd, _spawnedGroup=%s", veaf.p(_spawnedGroup, nil, {"route", "payload"})) local _dcsSpawnedGroup = Group.getByName(_spawnedGroup.name) -- LOGGING DISABLED WHEN COMPILING("result of dcs side getByName, _dcsSpawnedGroup=%s", veaf.p(_dcsSpawnedGroup, nil, {"route", "payload"})) -- LOGGING DISABLED WHEN COMPILING("result of dcs side getByName, _dcsSpawnedGroup.name=%s", _dcsSpawnedGroup:getName()) for index, unit in pairs(_dcsSpawnedGroup:getUnits()) do -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("veafSpawn.startCapWatchdog(capGroupName=%s)", veaf.p(capGroupName)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Looking in CAP zone for targets...") local timestamp = timer.getTime() local targetsList = pTargetsList or {} local numberOfTasksAddedByWatchdog = pNumberOfTasksAddedByWatchdog or 0 -- LOGGING DISABLED WHEN COMPILING("targetsList=%s", veaf.p(targetsList)) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("targetIsAirborne=%s", veaf.p(targetIsAirborne)) -- LOGGING DISABLED WHEN COMPILING("targetCoalition=%s", veaf.p(targetCoalition)) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("targetPosition=%s", veaf.p(targetPosition)) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("targetType=%s", veaf.p(targetType)) -- LOGGING DISABLED WHEN COMPILING("targetAttributes=%s", veaf.p(targetAttributes)) -- LOGGING DISABLED WHEN COMPILING("targetDistanceFromCapGroup=%s", veaf.p(targetDistanceFromCapGroup)) local targetPriority = nil if targetAttributes["Fighters"] or targetAttributes["Multirole fighters"] then -- LOGGING DISABLED WHEN COMPILING("Target is a Fighter") targetPriority = math.floor(targetDistanceFromCapGroup/2) elseif targetAttributes["Strategic bombers"] then -- LOGGING DISABLED WHEN COMPILING("Target is a strategic bomber") targetPriority = math.floor(targetDistanceFromCapGroup/1.5) + 10000 elseif targetAttributes["Bombers"] then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Target is an AWACS") targetPriority = math.floor(targetDistanceFromCapGroup/0.5) + 15000 elseif targetAttributes["Transports"] then -- LOGGING DISABLED WHEN COMPILING("Target is a Transport") targetPriority = math.floor(targetDistanceFromCapGroup/0.5) + 15000 elseif targetAttributes["Battle airplanes"] or targetAttributes["Battleplanes"] then -- LOGGING DISABLED WHEN COMPILING("Target is a generic Battleplane") targetPriority = math.floor(targetDistanceFromCapGroup/0.25) + 15000 elseif targetAttributes["Helicopters"] or targetAttributes["Attack helicopters"] or targetAttributes["Transport helicopters"] then -- LOGGING DISABLED WHEN COMPILING("Target is a Helicopter") targetPriority = math.floor(targetDistanceFromCapGroup/0.1) + 20000 else -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("redetection (same run) of targetName=%s", veaf.p(targetName)) if targetData.priority < targetPriority then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("redetection (previous run) of targetName=%s", veaf.p(targetName)) targetData.isNew = false end else -- new target! register in into the target list -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("CAP group %s is landed, destroying it and stopping watchdog", veaf.p(capGroupName)) return end local controller = capGroup:getController() if capInZone then -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("targetsList=%s", veaf.p(targetsList)) local foundTargets = false for targetId, targetData in pairs(targetsList) do if not foundTargets then -- only write that once! -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Target is outdated, landed or doesn't exist, removing it from the list") targetsList[targetId] = nil else -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("Watchdog found no targets, removing all tasks and prohibiting AA for CAP") while controller:hasTask() and numberOfTasksAddedByWatchdog > 0 do -- LOGGING DISABLED WHEN COMPILING("numberOfTasksAddedByWatchdog=%s", veaf.p(numberOfTasksAddedByWatchdog)) controller:resetTask() -- LOGGING DISABLED WHEN COMPILING("resetTask() called") numberOfTasksAddedByWatchdog = numberOfTasksAddedByWatchdog - 1 end controller:setOption(AI.Option.Air.id.PROHIBIT_AA, true) controller:setOption(0,3) --return fire end else -- LOGGING DISABLED WHEN COMPILING("CAP is outside of its area ! Discarding targets...") controller:setOption(AI.Option.Air.id.PROHIBIT_AA, true) controller:setOption(0,3) --return fire end -- LOGGING DISABLED WHEN COMPILING(string.format("Rescheduling watchdog in %s seconds", veafSpawn.CAP_WATCHDOG_DELAY)) -- LOGGING DISABLED WHEN COMPILING("===============================================================================") 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) -- LOGGING DISABLED WHEN COMPILING("veafSpawn.missionMasterAddRunnable(name=%s)",name) veafSpawn.missionMasterRunnables[veaf.ifnn(name, "upper")] = { code, parameters } end function veafSpawn.missionMasterRun(name) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafSecurity.executeCommandFromRemote()")) -- LOGGING DISABLED WHEN COMPILING(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilot= %s", veaf.p(_pilot))) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilotName= %s", veaf.p(_pilotName))) -- LOGGING DISABLED WHEN COMPILING(string.format("_unitName= %s", veaf.p(_unitName))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("_action=%s",veaf.p(_action))) -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("checkPassword(password = %s)",password)) local hash = sha1.hex(password) -- LOGGING DISABLED WHEN COMPILING(string.format("hash = [%s]",hash)) if level[hash] ~= nil then -- LOGGING DISABLED WHEN COMPILING("user authenticated") return true else -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafSecurity.getMarkerSecurityLevel([%s])",veaf.p(markId))) local _author = nil for _, panel in pairs(world.getMarkPanels( )) do -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("_author=%s",_author) local _user = veafRemote.getRemoteUser(_author) -- LOGGING DISABLED WHEN COMPILING(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.37.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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafAlias[%s]:dontEndWithComma()", veaf.p(self.name))) self:setEndsWithComma(false) return self end function VeafAlias:setDescription(value) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(message) return true end -- LOGGING DISABLED WHEN COMPILING(string.format("markId=[%s]",veaf.p(markId))) local command = self:getVeafCommand() for _, parameter in pairs(self:getRandomParameters()) do -- LOGGING DISABLED WHEN COMPILING(string.format("randomizing [%s]",parameter.name or "")) local value = math.random(parameter.low, parameter.high) -- LOGGING DISABLED WHEN COMPILING(string.format("got [%d]",value)) command = string.format("%s, %s %d",command, parameter.name, value) end if self:isEndsWithComma() then -- LOGGING DISABLED WHEN COMPILING("adding a comma") command = command .. ", " end local _bypassSecurity = bypassSecurity or self:isBypassSecurity() local command = command .. (remainingCommand or "") -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("VeafAliasForCombatMission[%s]:execute([%s])", veaf.p(self.name), veaf.p(remainingCommand)) local command = self:getVeafCommand() for _, parameter in pairs(self:getRandomParameters()) do -- LOGGING DISABLED WHEN COMPILING(string.format("randomizing [%s]",parameter.name or "")) local value = math.random(parameter.low, parameter.high) -- LOGGING DISABLED WHEN COMPILING(string.format("got [%d]",value)) command = string.format("%s, %s %d",command, parameter.name, value) end if self:isEndsWithComma() then -- LOGGING DISABLED WHEN COMPILING("adding a comma") command = command .. ", " end local _bypassSecurity = bypassSecurity or self:isBypassSecurity() local command = command .. (remainingCommand or "") -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("missionName=%s", veaf.p(missionName)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("VeafAliasForCombatZone[%s]:execute([%s])", veaf.p(self.name), veaf.p(remainingCommand)) local command = self:getVeafCommand() for _, parameter in pairs(self:getRandomParameters()) do -- LOGGING DISABLED WHEN COMPILING(string.format("randomizing [%s]",parameter.name or "")) local value = math.random(parameter.low, parameter.high) -- LOGGING DISABLED WHEN COMPILING(string.format("got [%d]",value)) command = string.format("%s, %s %d",command, parameter.name, value) end if self:isEndsWithComma() then -- LOGGING DISABLED WHEN COMPILING("adding a comma") command = command .. ", " end local _bypassSecurity = bypassSecurity or self:isBypassSecurity() local command = command .. (remainingCommand or "") -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("zoneName=%s", veaf.p(zoneName)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafShortcuts.GetAlias([%s])",aliasName or "")) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafShortcuts.ExecuteAlias([%s],[%s],[%s],[%s],[%s])", veaf.p(aliasName), veaf.p(delay), veaf.p(remainingCommand), veaf.p(position), veaf.p(coalition))) -- LOGGING DISABLED WHEN COMPILING(string.format("markId=[%s]",veaf.p(markId))) -- LOGGING DISABLED WHEN COMPILING(string.format("bypassSecurity=[%s]",veaf.p(bypassSecurity))) -- LOGGING DISABLED WHEN COMPILING(string.format("route=[%s]",veaf.p(route))) local alias = veafShortcuts.GetAlias(aliasName) if alias then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("event.idx = %s", veaf.p(event.idx))) if veafShortcuts.executeCommand(eventPos, event.text, invertedCoalition, event.idx) then -- Delete old mark. -- LOGGING DISABLED WHEN COMPILING(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafShortcuts.executeCommand(eventPos, eventText, eventCoalition, markId, bypassSecurity, spawnedGroups, route) -- LOGGING DISABLED WHEN COMPILING(string.format("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 = eventPos if coords and #coords > 0 then local _lat, _lon = veaf.computeLLFromString(coords) -- LOGGING DISABLED WHEN COMPILING(string.format("_lat=%s",veaf.p(_lat))) -- LOGGING DISABLED WHEN COMPILING(string.format("_lon=%s",veaf.p(_lon))) if _lat and _lon then position = coord.LLtoLO(_lat, _lon) -- LOGGING DISABLED WHEN COMPILING(string.format("position=%s",veaf.p(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 -- 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 -- LOGGING DISABLED WHEN COMPILING(string.format("veafShortcuts.markTextAnalysis(text=[%s])", text)) -- check for the alias starter if text:sub(1,1) == veafShortcuts.AliasStarter then -- LOGGING DISABLED WHEN COMPILING("found veafShortcuts.AliasStarter") -- extract alias and remainder local alias, coords, delay, remainder = text:match("(-[^#^!^ ^,]+)#?([^!^,^%s]*)!?(%d*)(.*)") -- LOGGING DISABLED WHEN COMPILING(string.format("alias=[%s]", veaf.p(alias))) -- LOGGING DISABLED WHEN COMPILING(string.format("coords=[%s]", veaf.p(coords))) -- LOGGING DISABLED WHEN COMPILING(string.format("delay=[%s]", veaf.p(delay))) -- LOGGING DISABLED WHEN COMPILING(string.format("remainder=[%s]", veaf.p(remainder))) if alias then -- LOGGING DISABLED WHEN COMPILING(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") :addRandomParameter("defense", 4, 5) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-samSR") :setDescription("Random short range SAM battery") :setVeafCommand("_spawn samgroup, skynet true") :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") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hq7_single") :setDescription("HQ-7 (Red Banner) launcher") :setVeafCommand("_spawn group, name hq7_single, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hq7noew") :setDescription("HQ-7 (Red Banner) battery without EWR") :setVeafCommand("_spawn group, name hq7-noew, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hq7eo") :setDescription("HQ-7EO (Red Banner) battery") :setVeafCommand("_spawn group, name hq7eo, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hq7eo_single") :setDescription("HQ-7EO (Red Banner) launcher") :setVeafCommand("_spawn group, name hq7eo_single, skynet true") :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") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa2") :setDescription("SA-2 Guideline (S-75 Dvina) battery") :setVeafCommand("_spawn group, name sa2, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa5") :setDescription("SA-5 Gammon (S-200 Dubna) battery") :setVeafCommand("_spawn group, name sa5, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa3") :setDescription("SA-3 Goa (S-125 Neva/Pechora) battery") :setVeafCommand("_spawn group, name sa3, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa6") :setDescription("SA-6 Gainful (2K12 Kub) battery") :setVeafCommand("_spawn group, name sa6, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa8") :setDescription("SA-8 Osa (9K33 Osa) sam vehicle") :setVeafCommand("_spawn group, name sa8_squad, skynet true") :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") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa10") :setDescription("SA-10 Grumble (S-300) battery") :setVeafCommand("_spawn group, name sa10, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa11") :setDescription("SA-11 Gadfly (9K37 Buk) battery") :setVeafCommand("_spawn group, name sa11, skynet true") :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") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-sa15") :setDescription("SA-15 Gauntlet (9K330 Tor) sam vehicle") :setVeafCommand("_spawn group, name sa15_squad, skynet true") :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("-roland") :setDescription("Roland battery with EWR (US by default)") :setVeafCommand("_spawn group, name roland, country USA, skynet true") :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") :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") :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") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-hawk") :setDescription("Hawk battery (US by default)") :setVeafCommand("_spawn group, name hawk, country USA, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-patriot") :setDescription("Patriot battery (US by default)") :setVeafCommand("_spawn group, name patriot, country USA, skynet true") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-stinger") :setDescription("Stinger manpad squad (US by default)") :setVeafCommand("_spawn group, name stinger_squad, country USA") :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") :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") :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-ewr") :setDescription("55G6 Mast EWR") :setVeafCommand("_spawn group, name ewr, skynet true") :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") :addRandomParameter("defense", 1, 5) :setBypassSecurity(false) ) veafShortcuts.AddAlias( VeafAlias:new() :setName("-aaa") :setDescription("Random AAA battery") :setVeafCommand("_spawn samgroup, skynet true, spacing 1") :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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafShortcuts.executeCommandFromRemote()")) -- LOGGING DISABLED WHEN COMPILING(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilot= %s", veaf.p(_pilot))) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilotName= %s", veaf.p(_pilotName))) -- LOGGING DISABLED WHEN COMPILING(string.format("_unitName= %s", veaf.p(_unitName))) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("_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 -- LOGGING DISABLED WHEN COMPILING(string.format("_alias=%s",veaf.p(_alias))) else -- parse the command local _coords, __alias = _command:match(veafShortcuts.RemoteCommandParser) _alias = __alias -- LOGGING DISABLED WHEN COMPILING(string.format("_coords=%s",veaf.p(_coords))) -- LOGGING DISABLED WHEN COMPILING(string.format("_alias=%s",veaf.p(_alias))) if _coords then _lat, _lon = veaf.computeLLFromString(_coords) -- LOGGING DISABLED WHEN COMPILING(string.format("_lat=%s",veaf.p(_lat))) -- LOGGING DISABLED WHEN COMPILING(string.format("_lon=%s",veaf.p(_lon))) end end if _alias then if _lat and _lon then local _pos = coord.LLtoLO(_lat, _lon) -- LOGGING DISABLED WHEN COMPILING(string.format("_pos=%s",veaf.p(_pos))) -- LOGGING DISABLED WHEN COMPILING(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()) ) -- LOGGING DISABLED WHEN COMPILING(s) -- LOGGING DISABLED WHEN COMPILING(veaf.p(dcsAirbase:getDesc())) ]] local veafAirbase = veafAirbase:create(dcsAirbase) if (veafAirbase) then table.insert(veafAirbases.Airbases, veafAirbase) end end -- LOGGING DISABLED WHEN COMPILING("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] -- LOGGING DISABLED WHEN COMPILING(string.format("Nearest airbase for [ %s ]: [ %s ] at %dm", dcsUnit:getName(), veafAirbase:toString(), nearestList[1][2])) return nearestList[1][1] else -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format(">>>>>>> unit - %s - type=%s", dcsUnitName, dcsUnitType)) -- LOGGING DISABLED WHEN COMPILING(veaf.p(dcsUnit:getDesc())) local desc = dcsUnit:getDesc() -- LOGGING DISABLED WHEN COMPILING(desc.Kmax) -- LOGGING DISABLED WHEN COMPILING(desc.displayName) end ]] elseif (iCategory == Airbase.Category.HELIPAD) then sDisplayName = sDisplayName .. "(FARP)" end -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("=======") -- LOGGING DISABLED WHEN COMPILING(string.format("%d, %.2f", iDcsNumber, nDcsHeading)) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("Airbases and runways initialized for theater " .. env.mission.theatre) for _, veafAirbase in pairs(veafAirbases.Airbases) do -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setZoneCenter(%s)", veaf.p(self.name), veaf.p(value)) self.zoneCenter = value return self end function AirWaveZone:setZoneCenterFromCoordinates(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setZoneRadius(%s)", veaf.p(self.name), veaf.p(value)) self.zoneRadius = value return self end function AirWaveZone:setDrawZone(value) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setDrawZone(%s)", veaf.p(self.name), veaf.p(value)) self.drawZone = value or false return self end function AirWaveZone:setDescription(value) -- LOGGING DISABLED WHEN COMPILING("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(...) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:resetWaves()", veaf.p(self.name)) self.waves = {} return self end function AirWaveZone:setMessageStart(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setOnStart()", veaf.p(self.name)) self.onStart = value return self end function AirWaveZone:setMessageWaitForHumans(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setOnWaitForHumans()", veaf.p(self.name)) self.onWaitForHumans = value return self end function AirWaveZone:setMessageWaitToDeploy(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setOnWaitToDeploy()", veaf.p(self.name)) self.onWaitToDeploy = value return self end function AirWaveZone:setMessageDeploy(value) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setMessageDeploy()", veaf.p(self.name)) self.messageDeploy = value return self end function AirWaveZone:setMessageDeployPlayers(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setOnDeploy()", veaf.p(self.name)) self.onDeploy = value return self end function AirWaveZone:setMessageDestroyed(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setOnDestroyed()", veaf.p(self.name)) self.onDestroyed = value return self end function AirWaveZone:setMessageOutsideOfZone(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setOnOutsideOfZone()", veaf.p(self.name)) self.onOutsideOfZone = value return self end function AirWaveZone:setMessageWon(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setOnWon()", veaf.p(self.name)) self.onWon = value return self end function AirWaveZone:setMessageLost(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setOnLost()", veaf.p(self.name)) self.onLost = value return self end function AirWaveZone:setMessageStop(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setOnStop()", veaf.p(self.name)) self.onStop = value return self end function AirWaveZone:setSilent(value) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setSilent(%s)", veaf.p(self.name), veaf.p(value)) self.silent = value or false return self end function AirWaveZone:setRespawnRadius(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setDelayBeforeActivation(%s)", veaf.p(self.name), veaf.p(value)) self.delayBeforeActivation = value return self end function AirWaveZone:setResetWhenDying(value) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setResetWhenDying(%s)", veaf.p(self.name), veaf.p(value)) self.resetWhenDying = value return self end function AirWaveZone:setMinimumAltitudeInFeet(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:disableOutsideOfZonePlayers()", veaf.p(self.name)) self.maxSecondsOutsideOfZonePlayers = nil return self end function AirWaveZone:_setState(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:setHandleCrippledEnemyUnitCallback()", veaf.p(self.name)) self.handleCrippledEnemyUnitCallback = callback return self end function AirWaveZone:reset() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:isEnemyWaveDead(%s)", veaf.p(self.name), veaf.p(waveNumber)) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:isEnemyGroupDead(%s)", veaf.p(self.name), veaf.p(waveNumber)) if not group then return true end -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:handleCrippledEnemyUnit(%s)", veaf.p(self.name), veaf.p(waveNumber)) if not unit then return end -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:check() -> self.state=%s", veaf.p(self.name), veaf.p(veafAirWaves.statusToString(self.state))) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("destroy out of zone player unitName=%s", veaf.p(unitName)) unit:destroy() else -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("waiting %s seconds before activation", veaf.p(self.delayBeforeActivation)) -- LOGGING DISABLED WHEN COMPILING("self.timeOfActivation=%s", veaf.p(self.timeOfActivation)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("waiting %s seconds before spawning next wave(s)", veaf.p(self.delayBeforeNextWave)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("AirWaveZone[%s]:deployWaves()", veaf.p(self.name)) self.spawnedGroupsNames = {} local groupsToDeployForTheseWaves = {} local lastDelay repeat self.currentWaveIndex = self.currentWaveIndex + 1 local groupsToDeploy, delay = self:chooseGroupsToDeploy() -- LOGGING DISABLED WHEN COMPILING("groupsToDeploy=%s", veaf.p(groupsToDeploy)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("spawning group [%s]", veaf.p(groupName)) local groupData = mist.getGroupData(groupName) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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') -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("assets[%s] = '%s'",theAsset.name, theAsset.description)) local text = theAsset.description .. " is not active nor alive" if group then -- LOGGING DISABLED WHEN COMPILING("found asset group") local nAlive = 0 for _, unit in pairs(group:getUnits()) do -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("startCarrierOperations()") -- LOGGING DISABLED WHEN COMPILING(string.format("Parameters for this command are : %s",veaf.p(parameters))) local carrierInfo, userUnitName = veaf.safeUnpack(parameters) local groupName, duration = veaf.safeUnpack(carrierInfo) -- LOGGING DISABLED WHEN COMPILING(string.format("Carrier groupName : %s",veaf.p(groupName))) -- LOGGING DISABLED WHEN COMPILING(string.format("duration : %s",veaf.p(duration))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("startPosition (raw) ="..veaf.vecToString(startPosition)) currentHeading = mist.utils.round(mist.utils.toDegree(mist.getHeading(carrierUnit, true)), 0) end -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("startPosition="..veaf.vecToString(startPosition)) -- LOGGING DISABLED WHEN COMPILING(veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("wind=%s", veaf.p(wind)) local windspeed = mist.vec.mag(wind) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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} -- LOGGING DISABLED WHEN COMPILING("polygon=%s", veaf.p(polygon)) -- LOGGING DISABLED WHEN COMPILINGQuad(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] -- LOGGING DISABLED WHEN COMPILING("lUnit:getName()=%s", veaf.p(lUnit:getName())) if mist.pointInPolygon(lUnit:getPosition().p, polygon) then obstructions[#obstructions + 1] = lUnit end end -- LOGGING DISABLED WHEN COMPILING("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)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("headingRad="..headingRad) -- LOGGING DISABLED WHEN COMPILING("length="..length) -- LOGGING DISABLED WHEN COMPILING("newWaypoint="..veaf.vecToString(newWaypoint)) -- LOGGING DISABLED WHEN COMPILINGArrow(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 -- LOGGING DISABLED WHEN COMPILING("carrier.heading = " .. carrier.heading .. " (true)") --carrier.heading_mag = dir + magdev -- LOGGING DISABLED WHEN COMPILING("carrier.heading = " .. carrier.heading_mag .. " (mag)") carrier.speed = veaf.round(speed * 1.94384, 0) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("found Pedro group") pedroUnit = Unit.getByName(carrier.pedroUnitName) if not pedroUnit then pedroUnit = pedroGroup:getUnits(1) end -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("Pedro WP1 = " .. veaf.vecToString(pedroWaypoint1)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("Pedro WP2 = " .. veaf.vecToString(pedroWaypoint2)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("found Tanker group") -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Tanker WP1 = " .. veaf.vecToString(tankerWaypoint1)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("Tanker WP2 = " .. veaf.vecToString(tankerWaypoint2)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("found tanker unit ; destroying it") tankerUnit:destroy() end end veafCarrierOperations.doOperations() end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Rebuild the radio menu function veafCarrierOperations.rebuildRadioMenu() -- LOGGING DISABLED WHEN COMPILING("veafCarrierOperations.rebuildRadioMenu()") -- find the carriers in the veafCarrierOperations.carriers table and prepare their menus for name, carrier in pairs(veafCarrierOperations.carriers) do -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("remove the submenu") end -- create the submenu -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING(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] -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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') -- LOGGING DISABLED WHEN COMPILING("carrier.missionRoute=%s", veaf.p(carrier.missionRoute)) if veafCarrierOperations.Trace then for num, point in pairs(carrier.missionRoute) do -- LOGGING DISABLED WHEN COMPILING(veafCarrierOperations.traceMarkerId, "CARRIER", string.format("[%s] point %d", name, tostring(num)), point, nil) end end end end end end function veafCarrierOperations.doOperations() -- LOGGING DISABLED WHEN COMPILING("veafCarrierOperations.doOperations()") -- find the carriers in the veafCarrierOperations.carriers table and check if they are operating for name, carrier in pairs(veafCarrierOperations.carriers) do -- LOGGING DISABLED WHEN COMPILING("checking " .. name) if carrier.conductingAirOperations then -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(name .. " will continue conducting operations for " .. remainingTime .. " more minutes") -- check and reset course veafCarrierOperations.continueCarrierOperations(name) end elseif carrier.stoppedAirOperations then carrier.conductingAirOperations = false -- LOGGING DISABLED WHEN COMPILING(name .. " stopped conducting operations") -- LOGGING DISABLED WHEN COMPILING(veafCarrierOperations.getDebugMarkersErasedAtEachStep(carrier.carrierUnitName)) carrier.stoppedAirOperations = false -- reset the carrier group route to its original route (set in the mission) if carrier.missionRoute then -- LOGGING DISABLED WHEN COMPILING(string.format("resetting carrier %s route", name)) -- LOGGING DISABLED WHEN COMPILING("carrier.missionRoute="..veaf.p(carrier.missionRoute)) local result = mist.goRoute(name, carrier.missionRoute) end else -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("veafCarrierOperations.operationsScheduler()") veafCarrierOperations.doOperations() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafCarrierOperations.executeCommandFromRemote()")) -- LOGGING DISABLED WHEN COMPILING(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilot= %s", veaf.p(_pilot))) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilotName= %s", veaf.p(_pilotName))) -- LOGGING DISABLED WHEN COMPILING(string.format("_unitName= %s", veaf.p(_unitName))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("_action=%s",veaf.p(_action))) -- LOGGING DISABLED WHEN COMPILING(string.format("_carrierName=%s",veaf.p(_carrierName))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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.14.1" -- 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" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- 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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("event.idx = %s", veaf.p(event.idx))) if veafCasMission.executeCommand(eventPos, event.text, invertedCoalition, event.idx) then -- Delete old mark. -- LOGGING DISABLED WHEN COMPILING(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafCasMission.executeCommand(eventPos, eventText, coalition, markId, bypassSecurity) -- LOGGING DISABLED WHEN COMPILING(string.format("veafCasMission.executeCommand(eventText=[%s])", eventText)) -- LOGGING DISABLED WHEN COMPILING(string.format("coalition=%s", veaf.p(coalition))) -- LOGGING DISABLED WHEN COMPILING(string.format("markId=%s", veaf.p(markId))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword password", val)) switch.password = val end if switch.casmission and key:lower() == "size" then -- Set size. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("_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 -- LOGGING DISABLED WHEN COMPILING("_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 -- LOGGING DISABLED WHEN COMPILING(string.format("group.units=%s", veaf.p(group.units))) end --- 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) -- LOGGING DISABLED WHEN COMPILING("_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 -- LOGGING DISABLED WHEN COMPILING("_actualDefense = " .. _actualDefense) local _groupDefinition = "generateAirDefenseGroup-BLUE-" if side == veafCasMission.SIDE_RED then _groupDefinition = "generateAirDefenseGroup-RED-" end _groupDefinition = _groupDefinition .. tostring(_actualDefense) -- LOGGING DISABLED WHEN COMPILING("_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 -- LOGGING DISABLED WHEN COMPILING("#group.units = " .. #group.units) return group end --- Generates a transport company and its air defenses function veafCasMission.generateTransportCompany(groupName, defense, side, size) -- LOGGING DISABLED WHEN COMPILING(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))) -- LOGGING DISABLED WHEN COMPILING(string.format("groupCount=%s", tostring(groupCount))) local group = { disposition = { h = groupCount, w = groupCount}, units = {}, description = groupName, groupName = groupName, } -- generate a transport company local transportType for _ = 1, groupCount do if veaf.config.ww2 then if side == veafCasMission.SIDE_BLUE then transportType = veaf.randomlyChooseFrom({"Bedford_MWD", "CCKW_353", "Willys_MB"}) else transportType = veaf.randomlyChooseFrom({"Blitz_36-6700A", "Horch_901_typ_40_kfz_21", "Kubelwagen_82", "Sd_Kfz_7", "Sd_Kfz_2" }) end else if side == veafCasMission.SIDE_BLUE then transportType = veaf.randomlyChooseFrom({"LUV HMMWV Jeep", "M 818", "M978 HEMTT Tanker", "Land_Rover_101_FC", "Land_Rover_109_S3"}) else transportType = veaf.randomlyChooseFrom({"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"}) end end table.insert(group.units, { transportType, random=true}) end -- add an air defense vehicle every 10 vehicles local nbDefense = groupCount / 10 + 1 if nbDefense == 0 then nbDefense = 1 end -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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)) -- LOGGING DISABLED WHEN COMPILING(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 armorType local armorRand for _ = 1, groupCount do if armor <= 2 then if veaf.config.ww2 then if side == veafCasMission.SIDE_BLUE then armorType = veaf.randomlyChooseFrom({"M30_CC", "M10_GMC"}) else armorType = veaf.randomlyChooseFrom({"Sd_Kfz_251", "Sd_Kfz_234_2_Puma"}) end else if side == veafCasMission.SIDE_BLUE then armorType = veaf.randomlyChooseFrom({'IFV Marder', 'MCV-80', 'IFV LAV-25', "M1134 Stryker ATGM", 'M-2 Bradley'}) else armorType = veaf.randomlyChooseFrom({"BTR-82A", 'BMP-1', 'BMP-1', "VAB_Mephisto", 'BMP-2'}) end end elseif armor == 3 then if veaf.config.ww2 then if side == veafCasMission.SIDE_BLUE then armorType = veaf.randomlyChooseFrom({"M30_CC", "M10_GMC", "Centaur_IV",}) else armorType = veaf.randomlyChooseFrom({"Sd_Kfz_251", "Sd_Kfz_234_2_Puma", "Elefant_SdKfz_184"}) end else if side == veafCasMission.SIDE_BLUE then armorType = veaf.randomlyChooseFrom({'IFV Marder', "VAB_Mephisto", "M-2 Bradley", 'MBT Leopard 1A3', "Chieftain_mk3"}) else armorType = veaf.randomlyChooseFrom({"BTR-82A", "VAB_Mephisto", 'BMP-2', 'T-55', "Chieftain_mk3"}) end end elseif armor == 4 then if veaf.config.ww2 then if side == veafCasMission.SIDE_BLUE then armorType = veaf.randomlyChooseFrom({"Centaur_IV", "Churchill_VII", "Cromwell_IV"}) else armorType = veaf.randomlyChooseFrom({"Pz_IV_H", "Tiger_I", "Tiger_II_H","Stug_III","Stug_IV"}) end else if side == veafCasMission.SIDE_BLUE then armorType = veaf.randomlyChooseFrom({'M-2 Bradley', 'MBT Leopard 1A3', "Merkava_Mk4", "M1128 Stryker MGS"}) else armorType = veaf.randomlyChooseFrom({"BTR-82A", "BMP-3", "Chieftain_mk3", 'T-72B'}) end end elseif armor >= 5 then if veaf.config.ww2 then if side == veafCasMission.SIDE_BLUE then armorType = veaf.randomlyChooseFrom({"Centaur_IV", "Churchill_VII", "Cromwell_IV", "M4_Sherman", "M4A4_Sherman_FF"}, armor-5) else armorType = veaf.randomlyChooseFrom({"Pz_IV_H", "Tiger_I", "Tiger_II_H","Stug_III","Stug_IV", "JagdPz_IV", "Jagdpanther_G1", "Pz_V_Panther_G"}, armor-5) end else if side == veafCasMission.SIDE_BLUE then armorType = veaf.randomlyChooseFrom({"Merkava_Mk4", "Challenger2", "Leclerc", "Leopard-2", 'M-1 Abrams'}, armor-5) else armorType = veaf.randomlyChooseFrom({"BMP-3", "ZTZ96B", 'T-72B3', 'T-80UD', 'T-90'}, armor-5) end end end table.insert(group.units, { armorType, random=true }) end -- 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 -- LOGGING DISABLED WHEN COMPILING(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)) -- LOGGING DISABLED WHEN COMPILING(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 for _ = 1, groupCount do local rand = math.random(3) local unitType = nil if rand == 1 then if side == veafCasMission.SIDE_BLUE then unitType = 'Soldier RPG' else unitType = "Paratrooper RPG-16" end elseif rand == 2 then if side == veafCasMission.SIDE_BLUE then unitType = "Soldier M249" else unitType = "Infantry AK ver3" end else if side == veafCasMission.SIDE_BLUE then unitType = "Soldier M4 GRG" else unitType = "Infantry AK ver2" end end table.insert(group.units, { unitType }) end -- add a transport vehicle or an APC/IFV if armor > 3 then if side == veafCasMission.SIDE_BLUE then table.insert(group.units, { "M-2 Bradley", cell=11, random=true }) else table.insert(group.units, { "BMP-2", cell=11, random=true }) end elseif armor > 0 then if side == veafCasMission.SIDE_BLUE then table.insert(group.units, { "IFV Marder", cell=11, random=true }) else table.insert(group.units, { "BTR-82A", cell=11, random=true }) end else if side == veafCasMission.SIDE_BLUE then table.insert(group.units, { "M 818", cell=11, random=true }) else table.insert(group.units, { "KAMAZ Truck", cell=11, random=true }) end end -- 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 -- LOGGING DISABLED WHEN COMPILING(string.format("veafCasMission.placeGroup(#groupDefinition.units=%d)",#groupDefinition.units)) -- process the group -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(string.format("#resultTable=%d",#resultTable)) return resultTable end --- Generates a complete CAS target group function veafCasMission.generateCasGroup(casGroupName, spawnSpot, size, defense, armor, spacing, side) -- LOGGING DISABLED WHEN COMPILING("side = " .. tostring(side)) side = side or veafCasMission.SIDE_RED local units = {} local zoneRadius = (size+spacing)*350 -- LOGGING DISABLED WHEN COMPILING("zoneRadius = " .. zoneRadius) -- generate between size-2 and size+1 infantry groups local infantryGroupsCount = math.random(math.max(1, size-2), size + 1) -- LOGGING DISABLED WHEN COMPILING("infantryGroupsCount = " .. infantryGroupsCount) for infantryGroupNumber = 1, infantryGroupsCount do local groupName = casGroupName .. " - Infantry Section " .. infantryGroupNumber local groupPosition = veaf.findPointInZone(spawnSpot, zoneRadius, false) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("armorPlatoonsCount = " .. armorPlatoonsCount) for armorGroupNumber = 1, armorPlatoonsCount do local groupName = casGroupName .. " - Armor Platoon " .. armorGroupNumber local groupPosition = veaf.findPointInZone(spawnSpot, zoneRadius, false) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("airDefenseGroupsCount = " .. airDefenseGroupsCount) for airDefenseGroupNumber = 1, airDefenseGroupsCount do local groupName = casGroupName .. " - Air Defense Group ".. airDefenseGroupNumber local groupPosition = veaf.findPointInZone(spawnSpot, zoneRadius, false) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("transportCompaniesCount = " .. transportCompaniesCount) for transportCompanyGroupNumber = 1, transportCompaniesCount do local groupName = casGroupName .. " - Transport Company " .. transportCompanyGroupNumber local groupPosition = veaf.findPointInZone(spawnSpot, zoneRadius, false) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("skipCasTarget START") -- destroy vehicles and infantry groups -- LOGGING DISABLED WHEN COMPILING("destroy CAS group") local group = Group.getByName(veafCasMission.casGroupName) if group and group:isExist() == true then group:destroy() end -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("remove the watchdog function") if veafCasMission.groupAliveCheckTaskID ~= 'none' then mist.removeFunction(veafCasMission.groupAliveCheckTaskID) end veafCasMission.groupAliveCheckTaskID = 'none' -- LOGGING DISABLED WHEN COMPILING("update the radio menu 1") veafRadio.delCommand(veafCasMission.rootPath, 'Target information') -- LOGGING DISABLED WHEN COMPILING("update the radio menu 2") veafRadio.delCommand(veafCasMission.rootPath, 'Skip current objective') -- LOGGING DISABLED WHEN COMPILING("update the radio menu 3") veafRadio.delCommand(veafCasMission.rootPath, 'Get current objective situation') -- LOGGING DISABLED WHEN COMPILING("update the radio menu 4") veafRadio.delSubmenu(veafCasMission.targetMarkersPath, veafCasMission.rootPath) veafRadio.refreshRadioMenu() -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(string.format("copying parameter %s : ",tostring(name))) copy.parameters[name]=value end return copy end --- --- setters and getters --- function VeafCombatMissionObjective:setName(value) -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatMissionObjective.setName([%s])",value or "")) self.name = value return self end function VeafCombatMissionObjective:getName() return self.name end function VeafCombatMissionObjective:setDescription(value) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatMissionObjective[%s].configureAsTimedObjective()",self:getName())) local function onCheck(mission, parameters) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatMissionObjective[%s].configureAsKillEnemiesObjective()",self:getName())) local function onCheck(mission, parameters) -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatMissionObjective.configureAsKillEnemiesObjective.onCheck()")) if mission:isActive() then local nbKillsToWin = parameters.nbKillsToWin local whatsInAKill = parameters.whatsInAKill -- LOGGING DISABLED WHEN COMPILING(string.format("nbKillsToWin = %d",nbKillsToWin)) -- LOGGING DISABLED WHEN COMPILING(string.format("whatsInAKill = %d",whatsInAKill)) local nbLiveUnits, nbDamagedUnits, nbDeadUnits = mission:getRemainingEnemies(whatsInAKill) -- LOGGING DISABLED WHEN COMPILING(string.format("nbLiveUnits = %d",nbLiveUnits)) -- LOGGING DISABLED WHEN COMPILING(string.format("nbDamagedUnits = %d",nbDamagedUnits)) -- LOGGING DISABLED WHEN COMPILING(string.format("nbDeadUnits = %d",nbDeadUnits)) if (nbKillsToWin == -1 and nbLiveUnits == 0) or (nbKillsToWin >= 0 and nbDeadUnits >= nbKillsToWin) then -- objective is achieved -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatMissionObjective[%s].configureAsPreventDestructionOfSceneryObjectsInZone()",self:getName())) local function onCheck(mission, parameters) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(veaf.serialize("killedObjects", killedObjects)) for _, object in pairs(killedObjects) do -- LOGGING DISABLED WHEN COMPILING(string.format("checking id_ = [%s]", object.object.id_)) if objects[object.object.id_] then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("processing groupName=%s",veaf.p(groupName))) local _group = Group.getByName(groupName) -- LOGGING DISABLED WHEN COMPILING(string.format("_group=%s",veaf.p(_group))) if _group then local _unit1 = _group:getUnit(1) -- LOGGING DISABLED WHEN COMPILING(string.format("_unit1=%s",veaf.p(_unit1))) if _unit1 then -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("%s units in group [%s]",tostring(self.spawnedUnitsCountByGroup[group:getName()]), tostring(group:getName()))) return self end function VeafCombatMission:getSpawnedGroups() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatMission[%s]:getSpawnedGroups()",self.name or "")) for _, group in pairs(self.spawnedGroups) do -- LOGGING DISABLED WHEN COMPILING(string.format("spawnedGroups[%s]",group:getName())) end return self.spawnedGroups end function VeafCombatMission:clearSpawnedGroups() self.spawnedGroups = {} return self end function VeafCombatMission:addObjective(objective) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatMission[%s]:unscheduleWatchdogFunction()",self.name or "")) if self.watchdogFunctionId then -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("processing unit [%s]",unit:getName())) -- LOGGING DISABLED WHEN COMPILING(string.format("veaf.getUnitLifeRelative(unit) = %f",veaf.getUnitLifeRelative(unit))) if veaf.getUnitLifeRelative(unit) == 1.0 then -- LOGGING DISABLED WHEN COMPILING(string.format("unit[%s] is alive",unit:getName())) groupLiveUnits = groupLiveUnits + 1 elseif veaf.getUnitLifeRelative(unit) > whatsInAKill then -- LOGGING DISABLED WHEN COMPILING(string.format("unit[%s] is damaged (%d %%)",unit:getName(), veaf.getUnitLifeRelative(unit)*100 )) groupDamagedUnits = groupDamagedUnits + 1 groupLiveUnits = groupLiveUnits + 1 else -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("groupLiveUnits = %d",groupLiveUnits)) -- LOGGING DISABLED WHEN COMPILING(string.format("groupDamagedUnits = %d",groupDamagedUnits)) -- LOGGING DISABLED WHEN COMPILING(string.format("groupDeadUnits = %d",groupDeadUnits)) nbLiveUnits = nbLiveUnits + groupLiveUnits nbDamagedUnits = nbDamagedUnits + groupDamagedUnits nbDeadUnits = nbDeadUnits + groupDeadUnits end -- LOGGING DISABLED WHEN COMPILING(string.format("nbLiveUnits = %d",nbLiveUnits)) -- LOGGING DISABLED WHEN COMPILING(string.format("nbDamagedUnits = %d",nbDamagedUnits)) -- LOGGING DISABLED WHEN COMPILING(string.format("nbDeadUnits = %d",nbDeadUnits)) return nbLiveUnits, nbDamagedUnits, nbDeadUnits end function VeafCombatMission:getInformation() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("processing element [%s]",missionElement:getName())) local chance = math.random(0, 100) if chance <= missionElement:getSpawnChance() then -- spawn the element -- LOGGING DISABLED WHEN COMPILING(string.format("chance hit (%d <= %d)",chance, missionElement:getSpawnChance())) for _, groupName in pairs(missionElement:getGroups()) do local _spawnPoint = missionElement.spawnPoints[groupName] -- LOGGING DISABLED WHEN COMPILING(string.format("_spawnPoint=%s",veaf.p(_spawnPoint))) local _spawnRadius = missionElement:getSpawnRadius() if (missionElement:getScale() > 1 and _spawnRadius < veafCombatMission.MinimumSpacingBetweenClones) then _spawnRadius = veafCombatMission.MinimumSpacingBetweenClones end -- LOGGING DISABLED WHEN COMPILING(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') -- LOGGING DISABLED WHEN COMPILING(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]) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("spawnedUnitName=%s",veaf.p(spawnedUnitName))) end end -- LOGGING DISABLED WHEN COMPILING(string.format("_group=%s",veaf.p(_group))) local _spawnedGroup = mist.dynAdd(_group) if _spawnedGroup then -- LOGGING DISABLED WHEN COMPILING(string.format("_spawnedGroup.name=%s",veaf.p(_spawnedGroup.name))) local _dcsSpawnedGroup = Group.getByName(_spawnedGroup.name) -- LOGGING DISABLED WHEN COMPILING(string.format("_spawnedGroup.name=%s",veaf.p(_dcsSpawnedGroup:getName()))) for _, unit in pairs(_dcsSpawnedGroup:getUnits()) do -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("veafHoundElint.addPlatformToSystem(%s)",veaf.p(_dcsSpawnedGroup:getName()))) veafHoundElint.addPlatformToSystem(_dcsSpawnedGroup) end end end end else -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatMission[%s]:desactivate()",self.name or "")) self:setActive(false) self:unscheduleWatchdogFunction() for _, group in pairs(self:getSpawnedGroups()) do -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("reset the radio submenu") veafRadio.clearSubmenu(self.radioRootPath) end -- populate the radio menu -- LOGGING DISABLED WHEN COMPILING("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.) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafCombatMission.GetMissionNumber([%s])",tostring(number))) local mission = veafCombatMission.missionsList[number] return mission end function veafCombatMission.GetMission(name) -- LOGGING DISABLED WHEN COMPILING(string.format("veafCombatMission.GetMission([%s])",name or "")) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafCombatMission.AddMission([%s])",mission:getName() or "")) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafCombatMission.AddMissionsWithSkill([%s])",mission:getName() or "")) -- LOGGING DISABLED WHEN COMPILING(string.format("skills=%s",veaf.p(skills))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("grouping missionName=%s", mission:getName())) if mission:isRadioMenuEnabled() then local regex = ("^([^/]+)/([^/]+)/(.+)$") local name, skill, scale = mission:getName():match(regex) -- LOGGING DISABLED WHEN COMPILING(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] = {} -- LOGGING DISABLED WHEN COMPILING(string.format("creating group %s", groupName)) end table.insert(missionGroups[groupName], mission) if mission:isActive() then -- LOGGING DISABLED WHEN COMPILING(string.format("mission %s is active", mission:getName())) -- LOGGING DISABLED WHEN COMPILING(string.format("activating group %s", groupName)) if not(activeGroups[groupName]) then activeGroups[groupName] = {} end if skill then -- LOGGING DISABLED WHEN COMPILING(string.format("activating skill %s", skill)) if not(activeGroups[groupName][skill]) then activeGroups[groupName][skill] = {} end if scale then -- LOGGING DISABLED WHEN COMPILING(string.format("activating scale %s", scale)) activeGroups[groupName][skill][scale] = true end end end end end -- LOGGING DISABLED WHEN COMPILING(string.format("missionGroups=%s",veaf.p(missionGroups))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("group by skill and scale") local skills = {} for _, mission in pairs(missions) do local regex = ("^([^/]+)/([^/]+)/(%d+)$") local name, skill, scale = mission:getName():match(regex) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format(" %s", scale)) mission.radioRootPath = scalePath mission:updateRadioMenu(true) end end end end --- Build the initial radio menu function veafCombatMission.buildRadioMenu() -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(string.format("processing groupName=%s",groupName)) missions[groupName] = {title=missionsInGroup[1]:getRadioMenuName(), sort=missionsInGroup[1]:getFriendlyName(), missions=missionsInGroup, activeGroups=activeGroups[groupName]} end -- LOGGING DISABLED WHEN COMPILING(string.format("missions=%s",veaf.p(missions))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("checking skills")) if not skills then -- LOGGING DISABLED WHEN COMPILING(string.format("skills is nil")) if radioMenuEnabled then skills = {"Good", "Excellent"} else skills = nil end end local scales = scales -- LOGGING DISABLED WHEN COMPILING(string.format("checking scales")) if not scales then -- LOGGING DISABLED WHEN COMPILING(string.format("scales is nil")) if radioMenuEnabled then scales = {1, 2} else scales = nil end end -- LOGGING DISABLED WHEN COMPILING(string.format("skills=(%s)", veaf.p(skills))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("jsonify()") -- LOGGING DISABLED WHEN COMPILING("key=%s", veaf.p(key)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafCombatMission.executeCommandFromRemote()")) -- LOGGING DISABLED WHEN COMPILING(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilot= %s", veaf.p(_pilot))) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilotName= %s", veaf.p(_pilotName))) -- LOGGING DISABLED WHEN COMPILING(string.format("_unitName= %s", veaf.p(_unitName))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("_action=%s",veaf.p(_action))) -- LOGGING DISABLED WHEN COMPILING(string.format("_missionName=%s",veaf.p(_missionName))) -- LOGGING DISABLED WHEN COMPILING(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.17.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.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 return active .. self:getFriendlyName() end function VeafCombatZone:setFriendlyName(value) self.friendlyName = value return self end function VeafCombatZone:getFriendlyName() return self.friendlyName 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:getNextGroupName() self.identifier = (self.identifier or 0) + 1 local name = string.format("%s-#%04d",self.missionEditorZoneName, self.identifier) return name end function VeafCombatZone:addSpawnedGroup(groupOrName) local groupName = groupOrName if type(groupName) ~= "string" then groupName = tostring(groupName) end -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatZone[%s]:getSpawnedGroups()",veaf.p(self.missionEditorZoneName))) -- LOGGING DISABLED WHEN COMPILING(veaf.serialize("self.spawnedGroups", self.spawnedGroups)) return self.spawnedGroups end function VeafCombatZone:clearSpawnedGroups() self.spawnedGroups = {} return self end function VeafCombatZone:addDelayedSpawner(id) -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("VeafCombatZone[%s]:getDelayedSpawners()", veaf.p(self.missionEditorZoneName)) -- LOGGING DISABLED WHEN COMPILING("self.delayedSpawners=%s", self.delayedSpawners) return self.delayedSpawners end function VeafCombatZone:clearDelayedSpawners() self.delayedSpawners = {} return self end function VeafCombatZone:addZoneElement(element) -- LOGGING DISABLED WHEN COMPILING(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:getZoneElements() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatZone[%s]:getZoneElement()",veaf.p(self.missionEditorZoneName))) -- LOGGING DISABLED WHEN COMPILING(veaf.serialize("self.elements", self.elements)) return self.elements end function VeafCombatZone:getZoneElementsGroups() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatZone[%s]:getChainedCombatZones() - Initializing",veaf.p(self.missionEditorZoneName))) self.chainedCombatZones = {} end -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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()) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("spawnRadius = [%d]", spawnRadius)) zoneElement:setSpawnRadius(spawnRadius) end if spawnChance then -- LOGGING DISABLED WHEN COMPILING(string.format("spawnChance = [%d]", spawnChance)) zoneElement:setSpawnChance(spawnChance) end if spawnCount then -- LOGGING DISABLED WHEN COMPILING(string.format("spawnCount = [%d]", spawnCount)) zoneElement:setSpawnCount(spawnCount) end if spawnGroup then -- LOGGING DISABLED WHEN COMPILING(string.format("spawnGroup = [%s]", spawnGroup)) zoneElement:setSpawnGroup(spawnGroup) end if spawnDelay then -- LOGGING DISABLED WHEN COMPILING(string.format("spawnDelay = [%s]", spawnDelay)) zoneElement:setSpawnDelay(spawnDelay) end if command then -- it's a fake unit transporting a VEAF command -- LOGGING DISABLED WHEN COMPILING(string.format("command = [%s]", command)) zoneElement:setVeafCommand(command) local groupName = unit:getGroup():getName() zoneElement:setName(groupName) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("objectCategory=%s", veaf.p(objectCategory)) if objectCategory == 1 then local unitCategory = Unit.getCategory(unit) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(string.format("adding group [%s]", groupName)) alreadyAddedGroups[groupName] = groupName zoneElement:setName(groupName) else -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("VeafCombatZone[%s]:spawnElement([%s], [%s])",veaf.p(self:getFriendlyName()), veaf.p(zoneElement:getName()), veaf.p(now)) -- LOGGING DISABLED WHEN COMPILING("zoneElement=%s", zoneElement) if not now and zoneElement:getSpawnDelay() and type(zoneElement:getSpawnDelay()) == "number" then -- self-schedule -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("spawning zoneElement=%s now", zoneElement:getName()) local position = zoneElement:getPosition() if zoneElement:getSpawnRadius() > 0 then -- LOGGING DISABLED WHEN COMPILING(string.format("position=[%s]",veaf.vecToString(position))) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnRadius=[%s]",zoneElement:getSpawnRadius())) local mistP = mist.getRandPointInCircle(position, zoneElement:getSpawnRadius()) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 = self:getNextGroupName() vars.route = mist.getGroupRoute(vars.gpName, 'task') vars.action = 'respawn' vars.point = position vars.renameUnitsSequentially = true local newGroup = mist.teleportToPoint(vars) if type(newGroup) == 'table' then -- LOGGING DISABLED WHEN COMPILING(string.format("[%s]:activate() - mist.teleportToPoint([%s])", self:getMissionEditorZoneName(), zoneElement:getName())) self:addSpawnedGroup(newGroup.name) veaf.readyForCombat(newGroup.name) else -- LOGGING DISABLED WHEN COMPILING(string.format("[%s]:activate() - mist.teleportToPoint([%s]) failed", self:getMissionEditorZoneName(), zoneElement:getName())) end elseif zoneElement:getVeafCommand() then -- LOGGING DISABLED WHEN COMPILING(string.format("executing command [%s] at position [%s]",zoneElement:getName(), veaf.vecToString(position))) local spawnedGroups = {} veafInterpreter.execute(zoneElement:getVeafCommand(), position, zoneElement:getCoalition(), nil, spawnedGroups) for _, newGroup in pairs(spawnedGroups) do -- LOGGING DISABLED WHEN COMPILING(string.format("[%s].addSpawnedGroup", zoneElement:getName())) self:addSpawnedGroup(newGroup) -- LOGGING DISABLED WHEN COMPILING(string.format("newGroup = [%s]", newGroup)) local route = zoneElement:getRoute() -- LOGGING DISABLED WHEN COMPILING(string.format("got route")) local result = mist.goRoute(newGroup, route) -- LOGGING DISABLED WHEN COMPILING(string.format("sent group on its way")) end end end end -- activate the zone function VeafCombatZone:activate() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatZone[%s]:activate()",self:getMissionEditorZoneName())) self:setActive(true) for _, zoneElementGroup in pairs(self:getZoneElementsGroups()) do -- LOGGING DISABLED WHEN COMPILING(string.format("processing spawnGroup [%s]",zoneElementGroup.spawnGroup)) local spawnCount = zoneElementGroup.spawnCount -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("tries = [%d]",tries)) tries = tries - 1 for i=1,#shuffledIndexes do local zoneElement = zoneElementGroup.elements[shuffledIndexes[i]] -- LOGGING DISABLED WHEN COMPILING(string.format("processing element [%s]",veaf.p(zoneElement))) if spawnCount > 0 then if not alreadySpawnedElements[zoneElement:getName()] then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("chance = [%d]",chance)) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnChance = [%d]",zoneElement:getSpawnChance())) if chance <= zoneElement:getSpawnChance() then -- LOGGING DISABLED WHEN COMPILING(string.format("chance hit (%d <= %d)",chance, zoneElement:getSpawnChance())) spawnCount = spawnCount - 1 alreadySpawnedElements[zoneElement:getName()]=true self:spawnElement(zoneElement) else -- LOGGING DISABLED WHEN COMPILING(string.format("chance missed (%d > %d)",chance, zoneElement:getSpawnChance())) end else -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatZone[%s]:desactivate()",veaf.p(self.missionEditorZoneName))) self:setActive(false) self:unscheduleWatchdogFunction() for _, delayedSpawner in pairs(self:getDelayedSpawners()) do -- LOGGING DISABLED WHEN COMPILING("unscheduling delayed spawner %s", delayedSpawner) mist.removeFunction(delayedSpawner) end self:clearDelayedSpawners() for _, groupName in pairs(self:getSpawnedGroups()) do -- LOGGING DISABLED WHEN COMPILING(string.format("trying to destroy group [%s]",groupName)) local group = Group.getByName(groupName) if not group then group = StaticObject.getByName(groupName) if group then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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} } -- LOGGING DISABLED WHEN COMPILING(string.format("volS=%s",veaf.p(volS))) local n=world.removeJunk(volS) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("nbUnitsB=%d",nbUnitsB)) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatZone[%s]:popSmoke()",veaf.p(self.missionEditorZoneName))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatZone[%s]:popFlare()",veaf.p(self.missionEditorZoneName))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatZone[%s]:updateRadioMenu(%s)",veaf.p(self.missionEditorZoneName), tostring(inBatch))) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("add the radio submenu") self.radioRootPath = veafRadio.addSubMenu(self:getRadioMenuName(self:isActive()), self.radioParentPath) end if shouldAddSubMenu then -- populate the radio menu -- LOGGING DISABLED WHEN COMPILING("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.) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("#unitsNames=%s", veaf.p(#unitsNames)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("#units=%s", veaf.p(#units)) for _, unit in pairs(units) do local unitName = unit:getName() local objectCategory = Object.getCategory(unit) local groupName = nil -- LOGGING DISABLED WHEN COMPILING(string.format("processing unit [%s]", unitName)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatOperation[%s]:addTaskingOrder(%s)",veaf.p(self.missionEditorZoneName), veaf.p(zone.missionEditorZoneName))) -- LOGGING DISABLED WHEN COMPILING(string.format("Adding combat zone %s to operation %s", zone.missionEditorZoneName, veaf.p(self.missionEditorZoneName))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatOperation[%s]:updatePrimaryTasks()",veaf.p(self.missionEditorZoneName))) -- LOGGING DISABLED WHEN COMPILING("Clear primary tasks") self.primaryTaskingOrders = {} -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Setting new primary tasks") self.primaryTaskingOrders = newPrimaryTasks end -- checks if primary tasks are completed to unlock next function VeafCombatOperation:completionCheck() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Primary task %s is completed",primaryTask:getZone():getFriendlyName())) completedTaskingOrderCount = completedTaskingOrderCount + 1 end end -- LOGGING DISABLED WHEN COMPILING(string.format("%s completed out of %s, previous was %s",completedTaskingOrderCount, #self.primaryTaskingOrders, self.currentCompletedTaskingOrderCount)) if completedTaskingOrderCount == #self.primaryTaskingOrders then -- LOGGING DISABLED WHEN COMPILING("Primary tasks complete") self:updatePrimaryTasks() self:updateRadioMenu() completedTaskingOrderCount = 0 if not self:isActive() then return self end end -- LOGGING DISABLED WHEN COMPILING("Still got work to do.") if completedTaskingOrderCount ~= self.currentCompletedTaskingOrderCount then -- LOGGING DISABLED WHEN COMPILING("New tasking order completed. Update radio.") self:updatePrimaryTasks() self:updateRadioMenu() end self.currentCompletedTaskingOrderCount = completedTaskingOrderCount -- reschedule self:scheduleWatchdogFunction() return self end function VeafCombatOperation:initialize() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("desactivate the operation") self:desactivate() return self end -- activate the operation function VeafCombatOperation:activate() -- LOGGING DISABLED WHEN COMPILING(string.format("VeafCombatOperation[%s]:activate()", veaf.p(self.missionEditorZoneName))) self:setActive(true) local primaryTasks = {} -- activates member combat zones and sets starting primary tasks -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("reset the radio submenu") veafRadio.clearSubmenu(self.radioRootPath) else -- LOGGING DISABLED WHEN COMPILING("add the radio submenu") self.radioRootPath = veafRadio.addSubMenu(self:getRadioMenuName(), menuToFill) end -- populate the radio menu -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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.) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafCombatZone.GetZone([%s])", veaf.p(zoneName))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("veafCombatZone.buildRadioMenu() - dumping names") for i = 1, #names do -- LOGGING DISABLED WHEN COMPILING("veafCombatZone.buildRadioMenu().names -> " .. names[i]) end for _, zoneName in pairs(names) do local zone = veafCombatZone.GetZone(zoneName) 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) -- LOGGING DISABLED WHEN COMPILING("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 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.7.4" -- trace level, specific to this module --veafGrass.LogLevel = "trace" veaf.loggers.new(veafGrass.Id, veafGrass.LogLevel) veafGrass.DelayForStartup = 2 veafGrass.RadiusAroundFarp = 2000 ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- 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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafGrass.buildGrassRunway()")) -- LOGGING DISABLED WHEN COMPILING(string.format("grassRunwayUnit=%s",veaf.p(grassRunwayUnit))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("buildFarpsUnits: testing " .. unit.type .. " " .. name) if name:upper():find('GRASS_RUNWAY') then grassRunwayUnits[name] = unit -- LOGGING DISABLED WHEN COMPILING(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") and name:upper():sub(1,5)=="FARP " then farpUnits[name] = unit -- LOGGING DISABLED WHEN COMPILING(string.format("found farpUnits[%s]= %s", name, veaf.p(unit))) end end -- LOGGING DISABLED WHEN COMPILING(string.format("farpUnits=%s",veaf.p(farpUnits))) -- LOGGING DISABLED WHEN COMPILING(string.format("grassRunwayUnits=%s",veaf.p(grassRunwayUnits))) for name, unit in pairs(farpUnits) do -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("veafGrass.fillAllFarpWarehouses()") local farpBases = {} local grassBases = {} local bases = world.getAirbases() for _, base in pairs(bases) do local name = base:getName() -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("veafGrass.fillFarpWarehouse()") -- LOGGING DISABLED WHEN COMPILING("farp=[%s]", veaf.p(farp)) local farpName = farp.name -- LOGGING DISABLED WHEN COMPILING("farpName=[%s]", veaf.p(farpName)) local result = farpName ~= nil if not result then result, farpName = pcall(Unit.getUnitName, farp) end -- LOGGING DISABLED WHEN COMPILING("farpName=[%s]", veaf.p(farpName)) if not result then result, farpName = pcall(Group.getGroupName, farp) end -- LOGGING DISABLED WHEN COMPILING("farpName=[%s]", veaf.p(farpName)) if not result then result, farpName = pcall(Object.getName, farp) end -- LOGGING DISABLED WHEN COMPILING("farpName=[%s]", veaf.p(farpName)) if farpName then local farpAirbase = Airbase.getByName(farpName) if farpAirbase then local farpWarehouse = farpAirbase:getWarehouse() if farpWarehouse then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("getLiquidAmount(%s) = %s", i, veaf.p(farpWarehouse:getLiquidAmount(i))) 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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("buildFarpUnits()")) -- LOGGING DISABLED WHEN COMPILING(string.format("farp=%s",veaf.p(farp))) -- LOGGING DISABLED WHEN COMPILING(string.format("grassRunwayUnits=%s",veaf.p(grassRunwayUnits))) -- LOGGING DISABLED WHEN COMPILING(string.format("hiddenOnMFD=%s",veaf.p(hiddenOnMFD))) -- LOGGING DISABLED WHEN COMPILING(string.format("noFarpMarkers=%s",veaf.p(noFarpMarkers))) -- LOGGING DISABLED WHEN COMPILING(string.format("code=%s",veaf.p(code))) -- LOGGING DISABLED WHEN COMPILING(string.format("freq=%s",veaf.p(freq))) -- LOGGING DISABLED WHEN COMPILING(string.format("mod=%s",veaf.p(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)) -- LOGGING DISABLED WHEN COMPILING(string.format("tacanGroupName=%s", tostring(tacanGroupName))) -- LOGGING DISABLED WHEN COMPILING(string.format("freq=%s", tostring(freq))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("txFreq=%s", tostring(txFreq))) -- LOGGING DISABLED WHEN COMPILING(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, } } -- LOGGING DISABLED WHEN COMPILING(string.format("setting %s", veaf.p(command))) local spawnedGroup = ctld.spawnRadioBeaconUnit(beaconPoint, farp.country, tacanGroupName, tacanGroupName) local controller = spawnedGroup:getController() controller:setCommand(command) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("farpNamedPoint=%s", veaf.p(farpNamedPoint))) veafNamedPoints.addPoint(farp.unitName or farp.name, farpNamedPoint) -- LOGGING DISABLED WHEN COMPILING(string.format("calling fillFarpWarehouse(%s)",name)) veafGrass.fillFarpWarehouse(farp) end --- --- called from veafEventHandler when a unit is created function veafGrass.onBirth(event) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("addPlatformToSystem(%s) to %s", veaf.p(groupName), veaf.p(veaf.ifnn(hound,"name")))) -- LOGGING DISABLED WHEN COMPILING(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"] -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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()) -- LOGGING DISABLED WHEN COMPILING(string.format("_p1=%s", veaf.p(_p1))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("adding a platform -> OK")) end end end end end end end -- LOGGING DISABLED WHEN COMPILING(string.format("batchMode = %s", veaf.p(batchMode))) -- LOGGING DISABLED WHEN COMPILING(string.format("dcsGroup=%s", veaf.p(mist.utils.deepCopy(dcsGroup)))) if Group.getByName(groupName) then for _, dcsUnit in pairs(dcsGroup:getUnits()) do -- LOGGING DISABLED WHEN COMPILING(string.format("dcsUnit.getName=%s", veaf.p(veaf.ifnn(dcsUnit, "getName")))) -- LOGGING DISABLED WHEN COMPILING(string.format("dcsUnit:isActive()=%s", veaf.p(dcsUnit:isActive()))) _addUnitToSystem(dcsUnit, dcsUnit:isActive()) end elseif StaticObject.getByName(groupName) then -- LOGGING DISABLED WHEN COMPILING("Group is Static") -- LOGGING DISABLED WHEN COMPILING(string.format("dcsGroup:isExist()=%s", veaf.p(dcsGroup:isExist()))) _addUnitToSystem(dcsGroup, dcsGroup:isExist()) end if didSomething and not(batchMode) then -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("initializeHoundSystem %s",tostring(hound.name))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("enabled markers") end if parameters.platformPositionErrors then hound:enablePlatformPosErrors() -- LOGGING DISABLED WHEN COMPILING("enabled platformPositionErrors") end if parameters.disableBDA then hound:disableBDA() -- LOGGING DISABLED WHEN COMPILING("disabled BDA") end if parameters.NATO_SectorCallsigns then hound:useNATOCallsignes(true) -- LOGGING DISABLED WHEN COMPILING("using NATO callsigns for zones") end if parameters.NATOmessages then hound:enableNATO() -- LOGGING DISABLED WHEN COMPILING("using NATO message format") end if parameters.ATISinterval and type(parameters.ATISinterval) == 'number' and parameters.ATISinterval > 0 then hound:setAtisUpdateInterval(parameters.ATISinterval) -- LOGGING DISABLED WHEN COMPILING(string.format("ATIS interval set to %s", veaf.p(parameters.ATISinterval))) end if parameters.preBriefedContacts then for _,name in pairs(parameters.preBriefedContacts) do -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("Pre-brief target added") end end end if parameters.debug then hound:onScreenDebug(true) -- LOGGING DISABLED WHEN COMPILING("Debug enabled") end end if parameters.sectors and type(parameters.sectors) == 'table' then -- LOGGING DISABLED WHEN COMPILING("Checking sectors...") for SectorName, sectorParameters in pairs(parameters.sectors) do local skip = false -- LOGGING DISABLED WHEN COMPILING(string.format("Given Sector Name : %s", veaf.p(SectorName))) -- LOGGING DISABLED WHEN COMPILING(string.format("Sector Params : %s", veaf.p(sectorParameters))) local SectorName = SectorName if not SectorName then -- LOGGING DISABLED WHEN COMPILING("No SectorName has been given...") SectorName = "default" skip = true end if SectorName ~= "default" and tostring(SectorName) then SectorName = tostring(SectorName) -- LOGGING DISABLED WHEN COMPILING(string.format("Retained Sector Name : %s", veaf.p(SectorName))) local sector = hound:addSector(SectorName) if sector then -- LOGGING DISABLED WHEN COMPILING("Added sector !") if SectorName:lower() ~= veafHoundElint.globalSectorName:lower() then -- LOGGING DISABLED WHEN COMPILING("Setting zone for sector...") hound:setZone(SectorName, SectorName) if not hound:getZone(SectorName) then -- LOGGING DISABLED WHEN COMPILING("Could not find zone for sector...") hound:removeSector(SectorName) skip = true else -- LOGGING DISABLED WHEN COMPILING("Zone found and set") end else -- LOGGING DISABLED WHEN COMPILING("No zone needs to be set for global sector") end else -- LOGGING DISABLED WHEN COMPILING("No sectors added, Hound problem...") skip = true end elseif SectorName ~= "default" then -- LOGGING DISABLED WHEN COMPILING("Given Sector Name could not be converted to string...") skip = true end if not skip then if veafHoundElint.hasSectorCallsign(sectorParameters) then -- LOGGING DISABLED WHEN COMPILING("Setting sector callsign...") if sectorParameters.callsign == true or not tostring(sectorParameters.callsign) then -- LOGGING DISABLED WHEN COMPILING("Using sector name as callsign") sectorParameters.callsign = SectorName end -- LOGGING DISABLED WHEN COMPILING(string.format("Callsign : %s", veaf.p(tostring(sectorParameters.callsign)))) hound:setCallsign(SectorName, tostring(sectorParameters.callsign)) end if veafHoundElint.hasTransmitterUnit(sectorParameters) then -- LOGGING DISABLED WHEN COMPILING("Setting transmitter unit...") -- LOGGING DISABLED WHEN COMPILING(string.format("transmitter unitName : %s", veaf.p(tostring(sectorParameters.transmitterUnit)))) hound:setTransmitter(SectorName, tostring(sectorParameters.transmitterUnit)) end if veafHoundElint.hasAtis(sectorParameters) then -- LOGGING DISABLED WHEN COMPILING("Setting up ATIS...") -- LOGGING DISABLED WHEN COMPILING(string.format("ATIS params : %s", veaf.p(sectorParameters.atis))) hound:enableAtis(SectorName, sectorParameters.atis) if sectorParameters.atis.reportEWR then -- LOGGING DISABLED WHEN COMPILING("ATIS will report EWRs as threats") hound:reportEWR(SectorName, sectorParameters.atis.reportEWR) end end if veafHoundElint.hasController(sectorParameters) then -- LOGGING DISABLED WHEN COMPILING("Setting up Controller...") -- LOGGING DISABLED WHEN COMPILING(string.format("Controller params : %s", veaf.p(sectorParameters.controller))) hound:enableController(SectorName, sectorParameters.controller) local textMode = not(veafHoundElint.hasControllerVoice(sectorParameters)) if textMode then -- LOGGING DISABLED WHEN COMPILING("Controller is text only") hound:enableText(SectorName) end end if veafHoundElint.hasNotifier(sectorParameters) then -- LOGGING DISABLED WHEN COMPILING("Setting up Notifier...") -- LOGGING DISABLED WHEN COMPILING(string.format("Notifier params : %s", veaf.p(sectorParameters.notifier))) hound:enableNotifier(SectorName, sectorParameters.notifier) end if veafHoundElint.hasNoAlerts(sectorParameters) then -- LOGGING DISABLED WHEN COMPILING("Disabling alerts for controller/ATIS") hound:disableAlerts(SectorName) end if veafHoundElint.hasNoTTS(sectorParameters) then -- LOGGING DISABLED WHEN COMPILING("Disabling TTS overall") hound:disableTTS(SectorName) end else -- LOGGING DISABLED WHEN COMPILING("Skipping sector") end end else -- LOGGING DISABLED WHEN COMPILING("No sectors to add/configure") end --activate the Hound system hound:systemOn() end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- local function createSystems(loadUnits, atMissionStart) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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") -- LOGGING DISABLED WHEN COMPILING(string.format("red=%s",veaf.p(red))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("copying parameter %s : ",tostring(name))) -- copy.parameters[name]=value -- end return copy end --- --- setters and getters --- function VeafMG_Protector:setName(value) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafMissileGuardian.AddGuardian([%s])",guardian:getName() or "")) return guardian end -- activate a guardian function veafMissileGuardian.ActivateGuardian(name, silent) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("group by skill and scale") local skills = {} for _, mission in pairs(missions) do local regex = ("^([^/]+)/([^/]+)/(%d+)$") local name, skill, scale = mission:getName():match(regex) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format(" %s", scale)) mission.radioRootPath = scalePath mission:updateRadioMenu(true) end end end end --- Build the initial radio menu function veafMissileGuardian.buildRadioMenu() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafMissileGuardian.executeCommandFromRemote()")) -- LOGGING DISABLED WHEN COMPILING(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilot= %s", veaf.p(_pilot))) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilotName= %s", veaf.p(_pilotName))) -- LOGGING DISABLED WHEN COMPILING(string.format("_unitName= %s", veaf.p(_unitName))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("_action=%s",veaf.p(_action))) -- LOGGING DISABLED WHEN COMPILING(string.format("_guardianName=%s",veaf.p(_missionName))) -- LOGGING DISABLED WHEN COMPILING(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.9.4" -- 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/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. -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword name = %s", val)) switch.groupName = val end if key:lower() == "speed" or key:lower() == "spd" then -- Set speed. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword alt = %d", val)) local nVal = tonumber(val) switch.altitude = nVal end if key:lower() == "teleport" then -- LOGGING DISABLED WHEN COMPILING("Keyword teleport found") switch.teleport = true end if key:lower() == "silent" then -- LOGGING DISABLED WHEN COMPILING("Keyword silent found") switch.silent = true end if key:lower() == "immortal" then -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("veafMove.moveGroup(groupName = " .. groupName .. ", speed = " .. speed .. ", altitude=".. altitude) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafMove.changeTanker(speed=%s, alt=%s)", tostring(speed), tostring(alt))) -- LOGGING DISABLED WHEN COMPILING(string.format("eventPos=%s",veaf.p(eventPos))) -- LOGGING DISABLED WHEN COMPILING(debugMarkers) local tankerUnit = nil local units = veaf.findUnitsInCircle(eventPos, 2000, false) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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] -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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] -- LOGGING DISABLED WHEN COMPILING("found point2") local foundOrbit = false local task1 = veaf.findInTable(point2, "task") if task1 then local tasks = task1.params.tasks if (tasks) then -- LOGGING DISABLED WHEN COMPILING("found %s tasks", veaf.p(#tasks)) for j, task in pairs(tasks) do -- LOGGING DISABLED WHEN COMPILING("found task #%s", veaf.p(j)) if task.params then -- LOGGING DISABLED WHEN COMPILING("has .params") if task.id and task.id == "Orbit" then -- LOGGING DISABLED WHEN COMPILING("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] -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("newpoint3="..veaf.p(point3)) -- replace whole mission -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafMove.moveTanker(groupName=%s, speed=%s, alt=%s, hdg=%s, distance=%s)",tostring(groupName), tostring(speed), tostring(alt), tostring(hdg), tostring(distance))) -- LOGGING DISABLED WHEN COMPILING(debugMarkers) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("tankerData : %s", veaf.p(tankerData)) local route = veaf.findInTable(tankerData, "route") local points = veaf.findInTable(route, "points") if points then -- LOGGING DISABLED WHEN COMPILING("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] -- LOGGING DISABLED WHEN COMPILING(string.format("point1=%s",veaf.p(point1))) local point2 = points[idxPoint2] -- LOGGING DISABLED WHEN COMPILING(string.format("point2=%s",veaf.p(point2))) local point3 = points[idxPoint3] -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("distance=%s",veaf.p(distance))) -- LOGGING DISABLED WHEN COMPILING(string.format("hdg=%s",veaf.p(hdg))) -- LOGGING DISABLED WHEN COMPILING(string.format("speed=%s",veaf.p(speed))) -- LOGGING DISABLED WHEN COMPILING(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 } -- LOGGING DISABLED WHEN COMPILING(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 } -- LOGGING DISABLED WHEN COMPILING(string.format("distance=%s",veaf.p(distance))) -- LOGGING DISABLED WHEN COMPILING(string.format("hdg=%s",veaf.p(hdg))) endLegPoint.x = startLegPoint.x + distance * math.cos(hdg) endLegPoint.y = startLegPoint.y + distance * math.sin(hdg) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("found " .. #tasks .. " tasks") for j, task in pairs(tasks) do -- LOGGING DISABLED WHEN COMPILING(string.format("found task #%s", veaf.p(j))) if task.params then -- LOGGING DISABLED WHEN COMPILING("has .params") if task.id and task.id == "Orbit" then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("newpoint3="..veaf.p(point3)) --actually move the group local delay = 0 -- teleport if the option is set if teleport then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("EscortData : %s", veaf.p(EscortData)) route_escort = veaf.findInTable(EscortData, "route") points_escort = veaf.findInTable(route_escort, "points") if points_escort then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Found correct escort Tasking ! Extracted Escorted ID : %s", task.params.groupId) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("Resetting %s mission", unitGroup:getName())) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("veafMove.moveAfac(groupName = " .. groupName .. ", speed = " .. speed .. ", alt = " .. alt) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("AFAC is dynamically spawned") afacData = veafSpawn.AFAC.missionData[coalition][number] isDynamicallySpawned = true end end end -- LOGGING DISABLED WHEN COMPILING("Found AFAC named " .. groupName .. " for move command") -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("FAC configuration valid on second to last WP") FACflag = true end end for _, task in pairs(tasks2_afac) do if task.id == "Orbit" then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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"} -- LOGGING DISABLED WHEN COMPILING(string.format("findAllTankers()")) local result = {} local units = mist.DBs.unitsByName -- local copy for faster execution for name, unit in pairs(units) do -- LOGGING DISABLED WHEN COMPILING(string.format("name=%s, unit.type=%s", veaf.p(name), veaf.p(unit.type))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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"] -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("searching for asset name %s", tankerUnitName)) local asset = veafAssets.get(tankerUnitName) if asset then tankerName = asset.description -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("namePoint(name = %s, coalition=%s)",name, coalition)) -- LOGGING DISABLED WHEN COMPILING("targetSpot=" .. veaf.vecToString(targetSpot)) -- find an existing point with the same name local existingPoint = veafNamedPoints.getPoint(name) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("created point %s", veaf.p(point))) end function veafNamedPoints.addPoint(name, point) if not point.y then point.y = 0 end -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("delPoint(name = %s)",name)) table.remove(veafNamedPoints.namedPoints, name:upper()) end function veafNamedPoints.getPoint(name) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(string.format("name=%s, distanceFromPlayer=%d",name, distanceFromPlayer)) if distanceFromPlayer < minDistance then minDistance = distanceFromPlayer closestPoint = point end end end -- LOGGING DISABLED WHEN COMPILING("closestPoint=%s",veaf.p(closestPoint)) return closestPoint end function veafNamedPoints.pointFromString(coordinatesString) -- LOGGING DISABLED WHEN COMPILING(string.format("pointFromString(coordinatesString = %s)",veaf.p(coordinatesString))) local _result = nil local _lat, _lon = veaf.computeLLFromString(coordinatesString) -- LOGGING DISABLED WHEN COMPILING(string.format("_lat=%s",veaf.p(_lat))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafNamedPoints.executeCommandFromRemote()")) -- LOGGING DISABLED WHEN COMPILING(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilot= %s", veaf.p(_pilot))) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilotName= %s", veaf.p(_pilotName))) -- LOGGING DISABLED WHEN COMPILING(string.format("_unitName= %s", veaf.p(_unitName))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("_action=%s",veaf.p(_action))) -- LOGGING DISABLED WHEN COMPILING(string.format("_pointName=%s",veaf.p(_pointName))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("processing city name=[%s]",name or "")) local point = coord.LLtoLO(data.latitude, data.longitude) -- LOGGING DISABLED WHEN COMPILING(string.format("point=[%s]",veaf.p(point))) point.hidden = true local name = data.display_name -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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 --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --[[ -- LOGGING DISABLED WHEN COMPILING("MODULE TESTS: " .. veafNamedPoints.Id) veafNamedPoints.addCities() veafNamedPoints.addAirbases() -- LOGGING DISABLED WHEN COMPILING(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") for _, veafNamedPoint in pairs(veafNamedPoints.namedPoints) do if (veaf.startsWith(_, "AIRBASE", false)) then -- LOGGING DISABLED WHEN COMPILING(">>> %s", veaf.p(veafNamedPoint)) end -- LOGGING DISABLED WHEN COMPILING(">>> %s", veaf.p(veafNamedPoint)) veafNamedPoints.markid = veafNamedPoints.markid + 1 trigger.action.markToAll(veafNamedPoints.markid, "VEAF - Point named ".. _, veafNamedPoint, true) end -- LOGGING DISABLED WHEN COMPILING(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") ]] ------------------ 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.2" -- 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.ennemyCoalitions = {} -- 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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setTriggerZone(%s)", veaf.p(self.name), veaf.p(value)) self.triggerZoneName = value return self end function VeafQRA:setZoneCenter(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setZoneCenter(%s)", veaf.p(self.name), veaf.p(value)) self.zoneCenter = value return self end function VeafQRA:setZoneCenterFromCoordinates(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setZoneRadius(%s)", veaf.p(self.name), veaf.p(value)) self.zoneRadius = value return self end function VeafQRA:setDescription(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setCoalition(%s)", veaf.p(self.name), veaf.p(value)) self.coalition = value return self end function VeafQRA:addEnnemyCoalition(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:addEnnemyCoalition(%s)", veaf.p(self.name), veaf.p(value)) self.ennemyCoalitions[value] = value return self end function VeafQRA:getEnnemyCoalition() local result = nil for coalition, _ in pairs(self.ennemyCoalitions) do result = coalition break end return result end function VeafQRA:setMessageStart(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setMessageStart(%s)", veaf.p(self.name), veaf.p(value)) self.messageStart = value return self end function VeafQRA:setOnStart(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setOnStart()", veaf.p(self.name)) self.onStart = value return self end function VeafQRA:setMessageDeploy(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setMessageDeploy(%s)", veaf.p(self.name), veaf.p(value)) self.messageDeploy = value return self end function VeafQRA:setOnDeploy(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setOnDeploy()", veaf.p(self.name)) self.onDeploy = value return self end function VeafQRA:setMessageDestroyed(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setMessageDestroyed(%s)", veaf.p(self.name), veaf.p(value)) self.messageDestroyed = value return self end function VeafQRA:setOnDestroyed(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setOnDestroyed()", veaf.p(self.name)) self.onDestroyed = value return self end function VeafQRA:setMessageReady(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setMessageReady(%s)", veaf.p(self.name), veaf.p(value)) self.messageReady = value return self end function VeafQRA:setOnReady(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setOnReady()", veaf.p(self.name)) self.onReady = value return self end function VeafQRA:setMessageOut(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setMessageOut(%s)", veaf.p(self.name), veaf.p(value)) self.messageOut = value return self end function VeafQRA:setOnOut(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setOnOut()", veaf.p(self.name)) self.onOut = value return self end function VeafQRA:setMessageResupplied(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setMessageResupplied(%s)", veaf.p(self.name), veaf.p(value)) self.messageResupplied = value return self end function VeafQRA:setOnResupplied(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setOnResupplied()", veaf.p(self.name)) self.onResupplied = value return self end function VeafQRA:setMessageAirbaseDown(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setMessageAirbaseDown(%s)", veaf.p(self.name), veaf.p(value)) self.messageAirbaseDown = value return self end function VeafQRA:setOnAirbaseDown(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setOnAirbaseDown()", veaf.p(self.name)) self.onAirbaseDown = value return self end function VeafQRA:setMessageAirbaseUp(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setMessageAirbaseUp(%s)", veaf.p(self.name), veaf.p(value)) self.messageAirbaseUp = value return self end function VeafQRA:setOnAirbaseUp(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setOnAirbaseUp()", veaf.p(self.name)) self.onAirbaseUp = value return self end function VeafQRA:setMessageStop(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setMessageStop(%s)", veaf.p(self.name), veaf.p(value)) self.messageStop = value return self end function VeafQRA:setOnStop(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setOnStop()", veaf.p(self.name)) self.onStop = value return self end function VeafQRA:setSilent(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setSilent(%s)", veaf.p(self.name), veaf.p(value)) self.silent = value or false return self end function VeafQRA:setDrawZone(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setDelayBeforeRearming(%s)", veaf.p(self.name), veaf.p(value)) self.delayBeforeRearming = value return self end function VeafQRA:setNoNeedToLeaveZoneBeforeRearming() -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setNoNeedToLeaveZoneBeforeRearming()", veaf.p(self.name)) self.noNeedToLeaveZoneBeforeRearming = true return self end function VeafQRA:setResetWhenLeavingZone() -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setResetWhenLeavingZone()", veaf.p(self.name)) self.resetWhenLeavingZone = true return self end function VeafQRA:setDelayBeforeActivating(value) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setDelayBeforeActivating(%s)", veaf.p(self.name), veaf.p(value)) self.delayBeforeActivating = value return self end function VeafQRA:setMinimumAltitudeInFeet(value) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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:_getEnemyHumanUnits() if not self._enemyHumanUnits then -- LOGGING DISABLED WHEN COMPILING("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.ennemyCoalitions[coalitionId] then if unit.category then if (unit.category == "plane") or (unit.category == "helicopter" and self.reactOnHelicopters) then -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:check()", veaf.p(self.name)) -- LOGGING DISABLED WHEN COMPILING("self.state=%s", veaf.p(veafQraManager.statusToString(self.state))) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Checking Warehousing...") -- LOGGING DISABLED WHEN COMPILING("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 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 else unitsInZone = veaf.findUnitsInCircle(self.zoneCenter, self.zoneRadius, false, unitNames) end 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 if (self.state == veafQraManager.STATUS_READY) and (unitsInZone and nbUnitsInZone > 0) then -- LOGGING DISABLED WHEN COMPILING(string.format("self.state set to veafQraManager.STATUS_READY_WAITINGFORMORE at timer.getTime()=%s", veaf.p(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 -- LOGGING DISABLED WHEN COMPILING("qraAlive=%s", veaf.p(qraAlive)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:setScheduledState(%s)", veaf.p(self.name), veaf.p(scheduledState)) if scheduledState == veafQraManager.STATUS_STOP then self.scheduled_state = veafQraManager.STATUS_STOP -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("QRA OUT scheduled") end return self end function VeafQRA:applyScheduledState() if self.scheduled_state and self.state ~= veafQraManager.STATUS_ACTIVE then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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.ennemyCoalitions) 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 -- LOGGING DISABLED WHEN COMPILING("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.ennemyCoalitions) 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() -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("QRA has %s/%s aircraft groups available", veaf.p(self.QRAcount), veaf.p(self.QRAmaxCount)) -- LOGGING DISABLED WHEN COMPILING("QRA has %s aircraft groups ready for resupply (-1 for infinite)", veaf.p(self.QRAresupplyMax)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("QRA can only be resupplied by %s aircraft groups", veaf.p(self.QRAresupplyMax)) end -- LOGGING DISABLED WHEN COMPILING("%s aircraft groups will be handled for resupply", veaf.p(resupplyAmount)) if resupplyAmount > 0 then self.isResupplying = true if self.delayBeforeQRAresupply > 0 then -- LOGGING DISABLED WHEN COMPILING("QRA will be resupplied in %s seconds", veaf.p(self.delayBeforeQRAresupply)) mist.scheduleFunction(VeafQRA.resupply, {self, resupplyAmount}, timer.getTime()+self.delayBeforeQRAresupply) else -- LOGGING DISABLED WHEN COMPILING("QRA is being resupplied...") self:resupply(resupplyAmount) end end end if self.QRAcount == 0 then -- LOGGING DISABLED WHEN COMPILING("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.ennemyCoalitions) 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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("QRA is going to be resupplied, old count is : %s", veaf.p(self.QRAcount)) self.QRAcount = self.QRAcount + resupplyAmount -- LOGGING DISABLED WHEN COMPILING("QRA was resupplied, new count is : %s", veaf.p(self.QRAcount)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("QRA now only has %s aircraft groups ready for resupply (-1 for infinite)", veaf.p(self.QRAresupplyMax)) if self.state == veafQraManager.STATUS_OUT then -- LOGGING DISABLED WHEN COMPILING("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.ennemyCoalitions) 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 -- LOGGING DISABLED WHEN COMPILING("QRA is no longer operating, resupply did not take place") end self.isResupplying = false end function VeafQRA:chooseGroupsToDeploy(nbUnitsInZone) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("group=%s", veaf.p(group)) table.insert(result, group) end groupsToDeploy = result end end return groupsToDeploy end function VeafQRA:deploy(nbUnitsInZone) -- LOGGING DISABLED WHEN COMPILING("VeafQRA[%s]:deploy()", veaf.p(self.name)) -- LOGGING DISABLED WHEN COMPILING("nbUnitsInZone=[%s]", veaf.p(nbUnitsInZone)) if self.minimumNbEnemyPlanes ~= -1 and self.minimumNbEnemyPlanes > nbUnitsInZone then -- LOGGING DISABLED WHEN COMPILING("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.ennemyCoalitions) 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("%[(.*)%](.*)") -- LOGGING DISABLED WHEN COMPILING("coords=%s", veaf.p(coords)) -- LOGGING DISABLED WHEN COMPILING("command=%s", veaf.p(command)) if coords then latDelta, lonDelta = coords:match("([%+-%d]+),%s*([%+-%d]+)") end end -- LOGGING DISABLED WHEN COMPILING("running command [%s]", veaf.p(command)) -- LOGGING DISABLED WHEN COMPILING("latDelta = [%s]", veaf.p(latDelta)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("group=%s", veaf.p(group)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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.ennemyCoalitions) 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 -- LOGGING DISABLED WHEN COMPILING("QRA will now see one of it's aicraft groups removed") self.QRAcount = self.QRAcount - 1 end end function VeafQRA:rearm(silent) -- LOGGING DISABLED WHEN COMPILING("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.ennemyCoalitions) 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() -- LOGGING DISABLED WHEN COMPILING("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.ennemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onStart then self.onStart() end return self end function VeafQRA:stop(silent) -- LOGGING DISABLED WHEN COMPILING("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.ennemyCoalitions) 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 veaf.loggers.get(veafQraManager.Id):info(string.format("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("veaf.config.SERVER_NAME=%s", veaf.p(veaf.config.SERVER_NAME))) _filename = _filename .. "-" .. veaf.config.SERVER_NAME end _filename = _filename .. ".log" -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(_message) veafSanctuary._recordAction(" INFO SCRIPTING: VEAF - T - " .. _message) end end function veafSanctuary.recordTraceShooting(message) if message and veafSanctuary.RecordTraceShooting then local _message = "SHOOTING - " .. message -- LOGGING DISABLED WHEN COMPILING(_message) veafSanctuary._recordAction(" INFO SCRIPTING: VEAF - T - " .. _message) end end function veafSanctuary.recordTraceTrespassing(message) if message and veafSanctuary.RecordTraceTrespassing then local _message = "TRESPASS - " .. message -- LOGGING DISABLED WHEN COMPILING(_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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("VeafSanctuaryZone[%s]:setPolygonFromUnits()", veaf.p(self.name))) -- LOGGING DISABLED WHEN COMPILING(string.format("markPositions = %s", veaf.p(markPositions))) local polygon = veaf.getPolygonFromUnits(unitNames) if polygon and #polygon > 0 then -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("VeafSanctuaryZone[%s]:deployDefenses()", veaf.p(self.name))) -- LOGGING DISABLED WHEN COMPILING(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)) -- LOGGING DISABLED WHEN COMPILING(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)) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("heading=%s", veaf.p(heading))) local heading1 = heading*math.random(70,130)/100 -- LOGGING DISABLED WHEN COMPILING(string.format("heading1=%s", veaf.p(heading1))) local heading1S = string.format(", hdg %s", tostring(veaf.invertHeading(heading1))) -- LOGGING DISABLED WHEN COMPILING(string.format("heading1S=%s", veaf.p(heading1S))) local heading2 = heading*math.random(70,130)/100 -- LOGGING DISABLED WHEN COMPILING(string.format("heading2=%s", veaf.p(heading2))) local heading2S = string.format(", hdg %s", tostring(veaf.invertHeading(heading2))) -- LOGGING DISABLED WHEN COMPILING(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)) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("spawnedGroupsNames = %s", veaf.p(spawnedGroupsNames))) end end end function VeafSanctuaryZone:cleanupDefenses() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("polygon mode") inZone = mist.pointInPolygon(position, self:getPolygon()) elseif self:getPosition() then -- LOGGING DISABLED WHEN COMPILING("circle and radius mode") local distanceFromCenter = ((position.x - self:getPosition().x)^2 + (position.z - self:getPosition().z)^2)^0.5 -- LOGGING DISABLED WHEN COMPILING(string.format("distanceFromCenter=%d, radius=%d", distanceFromCenter, self:getRadius())) inZone = distanceFromCenter < self:getRadius() end return inZone end function VeafSanctuaryZone:forgive(playerName) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("veafSanctuary.loop()") -- process all zones for _, zone in pairs(veafSanctuary.zonesList) do -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Matched by TR : " .. veafSkynet.getStringSkynetElement(skynetElement) .. " > " .. skynetDataName) return skynetData end if(skynetDatabaseMatchType(skynetElement.searchRadars, skynetData["searchRadar"])) then -- LOGGING DISABLED WHEN COMPILING("Matched by SR : " .. veafSkynet.getStringSkynetElement(skynetElement) .. " > " .. skynetDataName) return skynetData end end -- LOGGING DISABLED WHEN COMPILING("No match : " .. veafSkynet.getStringSkynetElement(skynetElement)) return nil end function veafSkynet.removeSkynetElement(skynetElement, veafSkynetNetwork) local iads = veafSkynetNetwork.iads -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Sam sites count: " .. #list) _removeSkynetElementFromList(list, skynetElement) --_removeSkynetElementFromList(iads:getEarlyWarningRadars(), skynetElement) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("veafSkynet.delayedActivate(%s)", veaf.p(networkName)) local network = veafSkynet.structure[networkName] if network then if network.delayedActivation then -- LOGGING DISABLED WHEN COMPILING(string.format("IADS %s already has a delayed activation", veaf.p(networkName))) else -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("veafSkynet._activateIADS(%s)", veaf.p(networkName)) local network = veafSkynet.structure[networkName] if network then network.delayedActivation = nil local iads = network.iads if iads then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("No group to find the nearest IADS site for") return false end local coa = dcsGroup:getCoalition() -- LOGGING DISABLED WHEN COMPILING(string.format("Ref coalition : %s", veaf.p(coa))) local iads = veafSkynet.getIadsOfCoalition(networkName, coa) if not iads then -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(string.format("networkName : %s", veaf.p(networkName))) local groupPos = veaf.getAveragePosition(dcsGroup) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Checked Site groupName : %s", veaf.p(site_name))) local groupAvgPosition = veaf.getAveragePosition(site_name) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("Distance between checked site and pointDefense : %s", veaf.p(distance))) if distance <= minDistance then -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Point defence: add as skynet") elementToDefend:addPointDefence(samSite) elseif (veafSkynet.PointDefenceMode == veafSkynet.PointDefenceModes.Dcs) then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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)] -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("DYNAMIC SPAWN monitoring ON") veafSkynet.monitorDynamicSpawnHandlerId = mist.addEventHandler(veafSkynet.OnDynamicSpawn) else if (veafSkynet.monitorDynamicSpawnHandlerId == nil) then return -- already inactive end -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("batchMode=%s forceEwr=%s PointDefense=%s", tostring(batchMode), tostring(forceEwr), tostring(pointDefense))) local defended_name = nil if pointDefense and not(forceEwr) then -- LOGGING DISABLED WHEN COMPILING(string.format("SAM is requested as pointDefense")) if pointDefense == true then -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(sLog .. " => unit type is not eligible for skynet") elseif(alreadyAddedGroups[groupName]) then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(sLog .. " => group not found for skynet ewr " .. veafSkynet.getStringSkynetElement(ewr)) elseif (dcsGroupEwr:getName() == dcsGroupName) then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("adding pointDefense to SAM -> OK")) iads:getSAMSiteByGroupName(defended_name):addPointDefence(addedSite) else -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("Recovered EWR name : %s", veaf.p(site_info[1].dcsName))) --local pointDefenses = site_info[1].pointDefences -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("SAM/EWR is forced EWR")) if addedSite then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(string.format("IADS named %s for coalition %s does not exist", veaf.p(networkName), veaf.p(coa))) return false end -- LOGGING DISABLED WHEN COMPILING(string.format("initializeIADS %s",tostring(iads:getCoalitionString()))) if debug then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("networkName= %s", veaf.p(networkName)) -- LOGGING DISABLED WHEN COMPILING("CoalitionID= %s", veaf.p(coa)) -- LOGGING DISABLED WHEN COMPILING("loadUnits= %s", veaf.p(loadUnits)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("creating network...") local iads = SkynetIADS:create(networkName) iads.coalitionID = coa if iads then if not veafSkynet.structure[networkName] then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Stored structure for network named %s :", veaf.p(networkName)) for index,_ in pairs(veafSkynet.structure[networkName]) do -- LOGGING DISABLED WHEN COMPILING("-> %s", veaf.p(index)) end -- LOGGING DISABLED WHEN COMPILING("Stored IADS structure for network named %s :", veaf.p(networkName)) for index,_ in pairs(veafSkynet.structure[networkName].iads) do -- LOGGING DISABLED WHEN COMPILING("-> %s", veaf.p(index)) end -- LOGGING DISABLED WHEN COMPILING("CoalitionID for network named %s :", veaf.p(networkName)) -- LOGGING DISABLED WHEN COMPILING("-> %s", veaf.p(veafSkynet.structure[networkName].iads.coalitionID)) -- LOGGING DISABLED WHEN COMPILING("-> %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 -- LOGGING DISABLED WHEN COMPILING("Stored structure for network named %s has IADS, deactivating", veaf.p(networkName)) if networkStructure.includeInRadio then -- LOGGING DISABLED WHEN COMPILING("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") -- LOGGING DISABLED WHEN COMPILING(string.format("includeRedInRadio=%s",veaf.p(includeRedInRadio))) -- LOGGING DISABLED WHEN COMPILING(string.format("debugRed=%s",veaf.p(debugRed))) -- LOGGING DISABLED WHEN COMPILING(string.format("includeBlueInRadio=%s",veaf.p(includeBlueInRadio))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("-> SAM")) veafSkynet.iadsSamUnitsTypes[unitType] = true end end end end end -- LOGGING DISABLED WHEN COMPILING(string.format("veafSkynet.iadsSamUnitsTypes=%s",veaf.p(veafSkynet.iadsSamUnitsTypes))) -- add EWR-capable units for _, unit in pairs(dcsUnits.DcsUnitsDatabase) do if unit then -- LOGGING DISABLED WHEN COMPILING(string.format("testing unit %s",veaf.p(unit.type))) if unit.attribute then -- LOGGING DISABLED WHEN COMPILING(string.format("unit.attribute = %s",veaf.p(unit.attribute))) if (unit.attribute["SAM SR"]) then veafSkynet.iadsEwrUnitsTypes[unit.type] = true -- LOGGING DISABLED WHEN COMPILING(string.format("-> EWR")) elseif (unit.attribute["EWR"]) then veafSkynet.iadsEwrUnitsTypes[unit.type] = true -- LOGGING DISABLED WHEN COMPILING(string.format("-> EWR")) elseif (unit.attribute["AWACS"]) then veafSkynet.iadsEwrUnitsTypes[unit.type] = true -- LOGGING DISABLED WHEN COMPILING(string.format("-> EWR")) end end end end -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Command center unit exploded: " .. dcsUnit:getName()) trigger.action.explosion(dcsUnit:getPosition().p, iExplosionStrength) end else -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("Executing VeafSkynetMonitorTaskContacts") local currentContacts = self:GetIadsCurrentContacts() -- LOGGING DISABLED WHEN COMPILING(self:ToString() .. " - currently tracking " .. #self.TrackedUnits .. " monitored units") for _, sContactName in pairs(currentContacts) do if (self:ContactIsToMonitor(sContactName) and not self:ContactIsTracked(sContactName)) then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(sInformation) elseif (self.OutputLogLevel == "trace") then -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Adding monitoring task: " .. task:ToString()) veafSkynetMonitor._monitoringTasks[task.Name] = task if (veafSkynetMonitor._monitoringThreadId == nil) then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Removing monitoring task: " .. veafSkynetMonitor._monitoringTasks[sTaskName]:ToString()) veafSkynetMonitor._monitoringTasks[sTaskName] = nil end if (veaf.length(veafSkynetMonitor._monitoringTasks) <= 0) then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(veaf.p(sunriseTime)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("%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 -- LOGGING DISABLED WHEN COMPILING("%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.7.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. -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword password", val)) switch.password = val end if switch.transportmission and key:lower() == "size" then -- Set size. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(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. -- LOGGING DISABLED WHEN COMPILING(string.format("Keyword from = %s", val)) switch.from = val end end return switch end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- CAS target group generation and management ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafTransportMission.doRadioTransmission(groupName) -- LOGGING DISABLED WHEN COMPILING("doRadioTransmission("..groupName..")") local group = Group.getByName(groupName) if group then -- LOGGING DISABLED WHEN COMPILING("Group is transmitting") local averageGroupPosition = veaf.getAveragePosition(groupName) -- LOGGING DISABLED WHEN COMPILING("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", "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, "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) -- LOGGING DISABLED WHEN COMPILING("generateTransportMission(size = %s, defense=%s, blocade=%d, from=%s)", veaf.p(size), veaf.p(defense), veaf.p(blocade), veaf.p(from)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("groupPosition=" .. veaf.vecToString(groupPosition)) groupPosition = { x = groupPosition.x, z = groupPosition.y, y = 0 } groupPosition = veaf.placePointOnLand(groupPosition) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Generating cargo") local startPosition = veaf.placePointOnLand(startPoint) -- LOGGING DISABLED WHEN COMPILING("startPosition=" .. veaf.vecToString(startPosition)) for i = 1, size do local spawnSpot = { x = startPosition.x + 50, z = startPosition.z + i * 10, y = startPosition.y } -- LOGGING DISABLED WHEN COMPILING("spawnSpot=" .. veaf.vecToString(spawnSpot)) local cargoType = veafTransportMission.CargoTypes[math.random(#veafTransportMission.CargoTypes)] local cargoName = veafTransportMission.BlueCargoName .. " #" .. i veafSpawn.doSpawnCargo(spawnSpot, 0, cargoType, "USA") end -- LOGGING DISABLED WHEN COMPILING("Done generating cargo") -- generate enemy air defense on the way if defense > 0 then -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Done generating air defense") end -- generate enemy blocade forces if blocade > 0 then -- LOGGING DISABLED WHEN COMPILING("Generating blocade") -- TODO -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("cleanupAfterMission()") -- destroy groups -- LOGGING DISABLED WHEN COMPILING("destroy friendly group") local group = Group.getByName(veafTransportMission.BlueGroupName) if group and group:isExist() == true then group:destroy() end -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("destroy enemy blocade group") group = Group.getByName(veafTransportMission.RedBlocadeGroupName) if group and group:isExist() == true then group:destroy() end -- remove the watchdog function -- LOGGING DISABLED WHEN COMPILING("remove the watchdog function") if veafTransportMission.friendlyGroupAliveCheckTaskID ~= 'none' then mist.removeFunction(veafTransportMission.friendlyGroupAliveCheckTaskID) end veafTransportMission.friendlyGroupAliveCheckTaskID = 'none' -- remove the watchdog function -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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.14.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 -- LOGGING DISABLED WHEN COMPILING("") -- LOGGING DISABLED WHEN COMPILING(" Group : " .. group.description) -- LOGGING DISABLED WHEN COMPILING("") 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 -- LOGGING DISABLED WHEN COMPILING(line1) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(line1) -- LOGGING DISABLED WHEN COMPILING(line2) -- LOGGING DISABLED WHEN COMPILING(line3) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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 = {} -- LOGGING DISABLED WHEN COMPILING("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] -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(string.format("hdg=%d",hdg)) for numUnit = 1, number do -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("result="..veaf.p(result)) return result end --- searches the database for a group having this alias (case insensitive) function veafUnits.findGroup(groupAlias) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("checkPositionForUnit()") -- LOGGING DISABLED WHEN COMPILING("spawnPosition=%s", spawnPosition) local vec2 = { x = spawnPosition.x, y = spawnPosition.z } -- LOGGING DISABLED WHEN COMPILING("vec2=%s", vec2) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Is Naval Static") IsNavalStatic = true end if landType == land.SurfaceType.WATER then -- LOGGING DISABLED WHEN COMPILING("landType = WATER") else -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("group = %s",veaf.p(group))) if not(hdg) then hdg = 0 -- default north end local hasDest = false or hasDest -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("totalWidth = %d",totalWidth)) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("HDG to nearest road : %s", veaf.p(nearestRoadHDG))) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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" }, }, { -- 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}, {"M1043 HMMWV Armament", cell = 2, fitToUnit}, {"Truck M939 Heavy", cell = 3, fitToUnit}, {"Diesel Power Station 5I57A", cell = 4, fitToUnit}, }, 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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("Keyword shells = %s", veaf.p(val)) local nVal = veaf.getRandomizableNumeric(val) -- LOGGING DISABLED WHEN COMPILING("shells = %s", veaf.p(nVal)) options.shells = nVal end if key:lower() == "radius" then -- Set the radius of the shelling -- LOGGING DISABLED WHEN COMPILING("Keyword radius = %s", veaf.p(val)) local nVal = veaf.getRandomizableNumeric(val) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("_lat=%s", veaf.p(_lat))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("event.idx = %s", veaf.p(event.idx))) if veafGroundAI.executeCommand(eventPos, event.text, coa, event.idx) then -- Delete old mark. -- LOGGING DISABLED WHEN COMPILING(string.format("Removing mark # %d.", event.idx)) trigger.action.removeMark(event.idx) end end function veafGroundAI.executeCommand(eventPos, eventText, eventCoalition, markId, bypassSecurity, spawnedGroups, route) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("options = %s", veaf.p(options))) if options then -- do the magic if options.verb == veafGroundAI.VERB_SET then -- LOGGING DISABLED WHEN COMPILING("options.verb == veafGroundAI.VERB_SET") local handlerName = options.name local group = options.group if group and handlerName then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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. -- LOGGING DISABLED WHEN COMPILING("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. -- LOGGING DISABLED WHEN COMPILING("Keyword name = %s", veaf.p(val)) options.name = val end if key:lower() == "order" then -- Set order -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("veafGroundAI.add([%s])", veaf.p(handler:getName())) veafGroundAI.handlers[handler:getName():lower()] = handler return handler end function veafGroundAI.remove(handler) -- LOGGING DISABLED WHEN COMPILING("veafGroundAI.remove([%s])", veaf.p(handler:getName())) veafGroundAI.handlers[handler:getName():lower()] = nil end function veafGroundAI.get(handlerName) -- LOGGING DISABLED WHEN COMPILING("veafGroundAI.get([%s])", veaf.p(handlerName)) local handler = veafGroundAI.handlers[handlerName:lower()] if handler then -- LOGGING DISABLED WHEN COMPILING("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.0" -- 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 -- LOGGING DISABLED WHEN COMPILING("Visibility new fog=%d", iVisibilityMeters) else -- LOGGING DISABLED WHEN COMPILING("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 nHumidity = _computeHumidity(vec3, clouds.BaseMeters, 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) -- LOGGING DISABLED WHEN COMPILING(this:toString(veafWeatherUnitSystem.Systems.Faa)) -- LOGGING DISABLED WHEN COMPILING(this:toString(veafWeatherUnitSystem.Systems.FaaNavy)) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("LASTE @ %f - W%dM %dT", iAltitudeFeet, iWindDirectionMagnetic, iWindDirection)) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("Preparing ATIS for airbase %s at %sZ", veafAirbase.Name, veafTime.toStringTime(dateTimeZulu, false))) local atisInEffect = veafWeatherAtis.ListInEffect[veafAirbase.Name] if (atisInEffect) then -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING(string.format("Current time %s: new ATIS", veafTime.toStringTime(dateTimeZulu, false))) atisInEffect = nil end end if (atisInEffect == nil) then atisInEffect = veafWeatherAtis:Create(veafAirbase, dateTimeZulu) -- LOGGING DISABLED WHEN COMPILING(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 -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("VeafFog[%s]:activate()", veaf.p(self.name)) veafWeather.setAndActivateFog(self) end function VeafFog:enable() -- LOGGING DISABLED WHEN COMPILING("VeafFog[%s]:enable()", veaf.p(self.name)) self.enabled = true -- store the existing fog parameters -- LOGGING DISABLED WHEN COMPILING("store the existing fog parameters") -- LOGGING DISABLED WHEN COMPILING("world.weather.getFogVisibilityDistance()=[%s]", veaf.p(world.weather.getFogVisibilityDistance())) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("set the fog to the programmed parameters") if self.forAnimationData then -- LOGGING DISABLED WHEN COMPILING("self.forAnimationData=[%s]", veaf.p(self.forAnimationData)) -- create an animation local animation = { self.forAnimationData } -- first reset fog animation -- LOGGING DISABLED WHEN COMPILING("first reset fog animation") world.weather.setFogAnimation({}) -- LOGGING DISABLED WHEN COMPILING("store the existing fog parameters") -- LOGGING DISABLED WHEN COMPILING("world.weather.getFogVisibilityDistance()=[%s]", veaf.p(world.weather.getFogVisibilityDistance())) -- LOGGING DISABLED WHEN COMPILING("world.weather.getFogThickness()=[%s]", veaf.p(world.weather.getFogThickness())) -- set the new fog animation world.weather.setFogAnimation(animation) elseif self.fogStaticData then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("do the first check, the method will reschedule itself") self:dynamicCheck() return self end function VeafFog:disable(dontRestore) -- LOGGING DISABLED WHEN COMPILING("VeafFog[%s]:disable()", veaf.p(self.name)) -- disable the scheduler if self.dynamicCheckFunctionScheduled then -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("date=[%s]", veaf.p(date)) -- Seasonal adjustment based on latitude and time of year local latitude, _, _ = coord.LOtoLL(position) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("sunrise_time=[%s:%s]", veaf.p(math.floor(sunrise_time/60)), veaf.p(math.fmod(sunrise_time,60))) -- LOGGING DISABLED WHEN COMPILING("morningPeak_time=[%s:%s]", veaf.p(math.floor(morningPeak_time/60)), veaf.p(math.fmod(morningPeak_time,60))) -- LOGGING DISABLED WHEN COMPILING("morningEnd_time=[%s:%s]", veaf.p(math.floor(morningEnd_time/60)), veaf.p(math.fmod(morningEnd_time,60))) -- LOGGING DISABLED WHEN COMPILING("eveningStart_time=[%s:%s]", veaf.p(math.floor(eveningStart_time/60)), veaf.p(math.fmod(eveningStart_time,60))) -- LOGGING DISABLED WHEN COMPILING("eveningPeak_time=[%s:%s]", veaf.p(math.floor(eveningPeak_time/60)), veaf.p(math.fmod(eveningPeak_time,60))) -- LOGGING DISABLED WHEN COMPILING("sunset_time=[%s:%s]", veaf.p(math.floor(sunset_time/60)), veaf.p(math.fmod(sunset_time,60))) -- LOGGING DISABLED WHEN COMPILING("daylight_duration=[%s]", veaf.p(daylight_duration)) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("weatherData.WindSpeedMps=[%s]", veaf.p(weatherData.WindSpeedMps)) -- LOGGING DISABLED WHEN COMPILING("temp_diff=[%s]", veaf.p(temp_diff)) -- LOGGING DISABLED WHEN COMPILING("base_prob=[%s]", veaf.p(base_prob)) -- LOGGING DISABLED WHEN COMPILING("season_factor=[%s]", veaf.p(season_factor)) -- LOGGING DISABLED WHEN COMPILING("diurnal_factor=[%s]", veaf.p(diurnal_factor)) -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING("minVisibility=[%s]", veaf.p(minVisibility)) -- LOGGING DISABLED WHEN COMPILING("maxVisibility=[%s]", veaf.p(maxVisibility)) -- LOGGING DISABLED WHEN COMPILING("minThickness=[%s]", veaf.p(minThickness)) -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("thickness=[%s]", veaf.p(thickness)) -- LOGGING DISABLED WHEN COMPILING("visibility=[%s]", veaf.p(visibility)) if self.dynamicFogIsAnimated then -- create an animation -- LOGGING DISABLED WHEN COMPILING("thickness=[%s]", veaf.p(thickness)) local animation = { VeafFog.DELAY_BETWEEN_DYNAMIC_CHECKS - VeafFog.DELAY_BETWEEN_DYNAMIC_CHECKS*0.1, visibility, thickness } -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING("fogObject=[%s]", veaf.p(fogObject)) -- disable the existing fog object if any if veafWeather.existingFog ~= nil then -- LOGGING DISABLED WHEN COMPILING("disable the existing fog object if any") -- LOGGING DISABLED WHEN COMPILING("veafWeather.existingFog=[%s]", veaf.p(veafWeather.existingFog)) veafWeather.existingFog:disable(true) end -- activate the new fog object -- LOGGING DISABLED WHEN COMPILING("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() -- LOGGING DISABLED WHEN COMPILING("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) -- LOGGING DISABLED WHEN COMPILING(string.format("veafWeather.executeCommandFromRemote()")) -- LOGGING DISABLED WHEN COMPILING(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilot= %s", veaf.p(_pilot))) -- LOGGING DISABLED WHEN COMPILING(string.format("_pilotName= %s", veaf.p(_pilotName))) -- LOGGING DISABLED WHEN COMPILING(string.format("_unitName= %s", veaf.p(_unitName))) -- LOGGING DISABLED WHEN COMPILING(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) -- LOGGING DISABLED WHEN COMPILING(string.format("_action=%s",veaf.p(_action))) -- LOGGING DISABLED WHEN COMPILING(string.format("_name=%s",veaf.p(_name))) -- LOGGING DISABLED WHEN COMPILING(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() -- LOGGING DISABLED WHEN COMPILING("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 -- LOGGING DISABLED WHEN COMPILING(veafWeatherAtis.getAtisString(veafAirbase)) -- LOGGING DISABLED WHEN COMPILING(veafWeatherData.getWeatherString(veafAirbase.DcsAirbase:getPoint())) end -- LOGGING DISABLED WHEN COMPILING(veaf.p(env.mission.weather.enable_fog)) -- LOGGING DISABLED WHEN COMPILING(veaf.p(world.weather.getFogVisibilityDistance())) -- LOGGING DISABLED WHEN COMPILING(veaf.p(world.weather.getFogThickness())) ]] ------------------ END script veafWeather.lua ------------------ ----------------------------------------------------------------------------------- -- END OF Veaf scripts 5.81.0;2025.03.29.14.39.59 -----------------------------------------------------------------------------------